Repository: star-39/moe-sticker-bot
Branch: master
Commit: ee93f7cab11a
Files: 64
Total size: 347.2 KB
Directory structure:
gitextract_b9bqh10n/
├── .github/
│ └── workflows/
│ ├── build_binaries.yml
│ ├── msb_nginx.yml
│ ├── msb_nginx_aarch64.sh
│ ├── msb_nginx_amd64.sh
│ ├── msb_oci.yml
│ ├── msb_oci_aarch64.sh
│ ├── msb_oci_amd64.sh
│ └── release_note.md
├── .gitignore
├── LICENSE
├── README.md
├── cmd/
│ ├── moe-sticker-bot/
│ │ └── main.go
│ └── msbimport/
│ └── main.go
├── core/
│ ├── admin.go
│ ├── commands.go
│ ├── config.go
│ ├── database.go
│ ├── define.go
│ ├── init.go
│ ├── message.go
│ ├── os_util.go
│ ├── states.go
│ ├── sticker.go
│ ├── sticker_download.go
│ ├── userdata.go
│ ├── util.go
│ ├── webapp.go
│ └── workers.go
├── deployments/
│ └── kubernetes_msb.yaml
├── go.mod
├── go.sum
├── pkg/
│ └── msbimport/
│ ├── README.md
│ ├── convert.go
│ ├── import.go
│ ├── import_kakao.go
│ ├── import_line.go
│ ├── typedefs.go
│ ├── util.go
│ └── workers.go
├── test/
│ ├── kakao_links.json
│ ├── line_links.json
│ └── tg_links.json
├── tools/
│ ├── msb_emoji.py
│ ├── msb_kakao_decrypt.py
│ └── msb_rlottie.py
└── web/
├── nginx/
│ ├── assetlinks.json
│ └── default.conf.template
└── webapp3/
├── .gitignore
├── README.md
├── package.json
├── public/
│ ├── index.html
│ ├── manifest.json
│ └── robots.txt
└── src/
├── App.css
├── App.js
├── Edit.js
├── Export.js
├── SortableSticker.js
├── Sticker.js
├── StickerGrid.js
├── StickerStyle.css
├── index.css
├── index.js
└── utils.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/build_binaries.yml
================================================
name: Build binaries for moe-sticker-bot and msbimport
on: push
jobs:
build_msb:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '>=1.20'
- name: Build MSB CLI
run: |
go version
GOOS=linux GOARCH=amd64 go build -o moe-sticker-bot_linux_amd64 cmd/moe-sticker-bot/main.go
GOOS=linux GOARCH=arm64 go build -o moe-sticker-bot_linux_aarch64 cmd/moe-sticker-bot/main.go
GOOS=windows GOARCH=amd64 go build -o moe-sticker-bot_windows_amd64.exe cmd/moe-sticker-bot/main.go
GOOS=windows GOARCH=arm64 go build -o moe-sticker-bot_windows_aarch64.exe cmd/moe-sticker-bot/main.go
GOOS=darwin GOARCH=amd64 go build -o moe-sticker-bot_macos_amd64 cmd/moe-sticker-bot/main.go
GOOS=darwin GOARCH=arm64 go build -o moe-sticker-bot_macos_aarch64 cmd/moe-sticker-bot/main.go
- name: Upload Artifacts
uses: actions/upload-artifact@v3.1.2
with:
name: msb_bins
path: moe-sticker-bot*
build_msbimport:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '>=1.20'
- name: Build Msbimport CLI
run: |
go version
GOOS=linux GOARCH=amd64 go build -o msbimport_linux_amd64 cmd/msbimport/main.go
GOOS=linux GOARCH=arm64 go build -o msbimport_linux_aarch64 cmd/msbimport/main.go
GOOS=windows GOARCH=amd64 go build -o msbimport_windows_amd64.exe cmd/msbimport/main.go
GOOS=windows GOARCH=arm64 go build -o msbimport_windows_aarch64.exe cmd/msbimport/main.go
GOOS=darwin GOARCH=amd64 go build -o msbimport_macos_amd64 cmd/msbimport/main.go
GOOS=darwin GOARCH=arm64 go build -o msbimport_macos_aarch64 cmd/msbimport/main.go
- name: Upload Artifacts
uses: actions/upload-artifact@v3.1.2
with:
name: msbimport_bins
path: msbimport*
release:
name: Release
if: startsWith(github.ref, 'refs/tags/')
needs: [ build_msb, build_msbimport ]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Download artifacts
uses: actions/download-artifact@v3
with:
path: releases/
- name: Publish Release
uses: softprops/action-gh-release@v1
with:
generate_release_notes: true
# append_body: true
body_path: ".github/workflows/release_note.md"
files: |
releases/*/*
================================================
FILE: .github/workflows/msb_nginx.yml
================================================
name: Build nginx for @moe-sticker-bot
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch:
jobs:
build_amd64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18.x'
- name: Build nginx container amd64
run: |
bash .github/workflows/msb_nginx_amd64.sh ${{ secrets.GITHUB_TOKEN }}
build_aarch64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18.x'
- name: Build nginx container aarch64
run: |
bash .github/workflows/msb_nginx_aarch64.sh ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/msb_nginx_aarch64.sh
================================================
#!/usr/bin/bash
GITHUB_TOKEN=$1
buildah login -u star-39 -p $GITHUB_TOKEN ghcr.io
#AArch64
c1=$(buildah from --arch=arm64 docker://arm64v8/nginx:latest)
# Copy nginx template and app link validation json.
buildah copy $c1 web/nginx/default.conf.template /etc/nginx/templates/
buildah copy $c1 web/nginx/assetlinks.json /www/.well-known/assetlinks.json
# Build react app
cd web/webapp3/
npm install
PUBLIC_URL=/webapp REACT_APP_HOST=msb39.eu.org npm run build
buildah copy $c1 build/ /webapp
cd ../..
buildah commit $c1 moe-sticker-bot:msb_nginx_aarch64
buildah push moe-sticker-bot:msb_nginx_aarch64 ghcr.io/star-39/moe-sticker-bot:msb_nginx_aarch64
================================================
FILE: .github/workflows/msb_nginx_amd64.sh
================================================
#!/usr/bin/bash
GITHUB_TOKEN=$1
buildah login -u star-39 -p $GITHUB_TOKEN ghcr.io
#AMD64
c1=$(buildah from docker://nginx:latest)
# Copy nginx template and app link validation json.
buildah run $c1 -- mkdir -p /etc/nginx/templates
buildah run $c1 -- mkdir -p /www/.well-known
buildah copy $c1 web/nginx/default.conf.template /etc/nginx/templates/
buildah copy $c1 web/nginx/assetlinks.json /www/.well-known/assetlinks.json
# Build react app
cd web/webapp3/
npm install
PUBLIC_URL=/webapp REACT_APP_HOST=msb39.eu.org npm run build
buildah copy $c1 build/ /webapp
cd ../..
buildah commit $c1 moe-sticker-bot:msb_nginx
buildah push moe-sticker-bot:msb_nginx ghcr.io/star-39/moe-sticker-bot:msb_nginx
================================================
FILE: .github/workflows/msb_oci.yml
================================================
name: Build OCI container for moe-sticker-bot
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
build_amd64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '>=1.20'
- name: Build MSB OCI container amd64
run: |
go version
bash .github/workflows/msb_oci_amd64.sh ${{ secrets.GITHUB_TOKEN }}
build_aarch64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '>=1.20'
- name: Build MSB OCI container aarch64
run: |
go version
bash .github/workflows/msb_oci_aarch64.sh ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/msb_oci_aarch64.sh
================================================
#!/bin/sh
GITHUB_TOKEN=$1
buildah login -u star-39 -p $GITHUB_TOKEN ghcr.io
# AArch64
#################################
if false ; then
c1=$(buildah from --arch=arm64 docker://lopsided/archlinux-arm64v8:latest)
buildah run $c1 -- pacman -Sy
buildah run $c1 -- pacman --noconfirm -S libwebp libheif imagemagick curl gifsicle libarchive python python-pip make gcc
buildah run $c1 -- pip3 install emoji rlottie-python Pillow --break-system-packages
buildah run $c1 -- pacman --noconfirm -Rsc make gcc python-pip
buildah run $c1 -- sh -c 'yes | pacman -Scc'
buildah config --cmd '/moe-sticker-bot' $c1
buildah commit $c1 moe-sticker-bot:base_aarch64
buildah push moe-sticker-bot:base_aarch64 ghcr.io/star-39/moe-sticker-bot:base_aarch64
fi
#################################
# Build container image.
c1=$(buildah from --arch=arm64 ghcr.io/star-39/moe-sticker-bot:base_aarch64)
# Install static build of ffmpeg.
curl -JOL "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linuxarm64-gpl.tar.xz"
tar -xvf ffmpeg-master-latest-linuxarm64-gpl.tar.xz
buildah copy $c1 ffmpeg-master-latest-linuxarm64-gpl/bin/ffmpeg /usr/local/bin/ffmpeg
# Build MSB go bin
GOOS=linux GOARCH=arm64 go build -o moe-sticker-bot cmd/moe-sticker-bot/main.go
buildah copy $c1 moe-sticker-bot /moe-sticker-bot
# Copy tools.
buildah copy $c1 tools/msb_kakao_decrypt.py /usr/local/bin/msb_kakao_decrypt.py
buildah copy $c1 tools/msb_emoji.py /usr/local/bin/msb_emoji.py
buildah copy $c1 tools/msb_rlottie.py /usr/local/bin/msb_rlottie.py
buildah commit $c1 moe-sticker-bot:aarch64
buildah push moe-sticker-bot:aarch64 ghcr.io/star-39/moe-sticker-bot:aarch64
================================================
FILE: .github/workflows/msb_oci_amd64.sh
================================================
#!/bin/sh
GITHUB_TOKEN=$1
buildah login -u star-39 -p $GITHUB_TOKEN ghcr.io
# AMD64
#################################
if true ; then
c1=$(buildah from docker://archlinux:latest)
buildah run $c1 -- pacman -Sy
buildah run $c1 -- pacman --noconfirm -S libwebp libheif imagemagick curl gifsicle libarchive python python-pip make gcc
buildah run $c1 -- pip3 install emoji rlottie-python Pillow --break-system-packages
buildah run $c1 -- pacman --noconfirm -Rsc make gcc python-pip
buildah run $c1 -- sh -c 'yes | pacman -Scc'
buildah config --cmd '/moe-sticker-bot' $c1
buildah commit $c1 moe-sticker-bot:base
buildah push moe-sticker-bot:base ghcr.io/star-39/moe-sticker-bot:base
fi
#################################
# Build container image.
c1=$(buildah from ghcr.io/star-39/moe-sticker-bot:base)
# Install static build of ffmpeg.
curl -JOL "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz"
tar -xvf ffmpeg-master-latest-linux64-gpl.tar.xz
buildah copy $c1 ffmpeg-master-latest-linux64-gpl/bin/ffmpeg /usr/local/bin/ffmpeg
# Build MSB go bin
go build -o moe-sticker-bot cmd/moe-sticker-bot/main.go
buildah copy $c1 moe-sticker-bot /moe-sticker-bot
# Copy tools.
buildah copy $c1 tools/msb_kakao_decrypt.py /usr/local/bin/msb_kakao_decrypt.py
buildah copy $c1 tools/msb_emoji.py /usr/local/bin/msb_emoji.py
buildah copy $c1 tools/msb_rlottie.py /usr/local/bin/msb_rlottie.py
buildah commit $c1 moe-sticker-bot:latest
buildah push moe-sticker-bot ghcr.io/star-39/moe-sticker-bot:amd64
buildah push moe-sticker-bot ghcr.io/star-39/moe-sticker-bot:latest
================================================
FILE: .github/workflows/release_note.md
================================================
See full changelog on https://github.com/star-39/moe-sticker-bot#changelog
__moe-sticker-bot*__ is the binary for the bot itself.
__msbimport*__ is the msbimport CLI utility to download and convert LINE/Kakao stickers from share link.
Remember to install required dependencies!
* ImageMagick
* bsdtar (libarchive-tools)
* ffmpeg
* curl
* python3 (optional, for following tools)
* [msb_kakao_decrypt.py](https://github.com/star-39/moe-sticker-bot/tree/master/tools/msb_kakao_decrypt.py) (optional, for decrypting animated kakao)
moe-sticker-bot requires more dependencies:
* gifsicle (optional, for converting GIF)
* [msb_emoji.py](https://github.com/star-39/moe-sticker-bot/tree/master/tools/msb_emoji.py) (optional, for emoji assign)
* [msb_rlottie.py](https://github.com/star-39/moe-sticker-bot/tree/master/tools/msb_rlottie.py) (optional, for converting TGS)
================================================
FILE: .gitignore
================================================
.vscode
.env
.venv
__debug*
*_data/
.DS_Store
deploy.yaml
tmp/
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
================================================
FILE: README.md
================================================
# [@moe_sticker_bot](https://t.me/moe_sticker_bot)
[](https://pkg.go.dev/github.com/star-39/moe-sticker-bot)   
[](https://t.me/moe_sticker_bot)
---
Use 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.
---
Telegram用萌萌貼圖BOT。
匯入或下載LINE和kakaotalk貼圖包到Telegram. 使用自己的圖片和影片創建Telegram貼圖包或表情貼並管理.
下載Telegram貼圖包/GIF。
## Features/功能
* Import LINE or kakao stickers to Telegram without effort, you can batch or separately assign emojis.
* Create your own sticker set or CustomEmoji with your own images or videos in any format.
* Support mixed-format sticker set. You can put animated and static stickers in the same set.
* Batch download and convert Telegram stickers or GIFs to original or common formats.
* Export Telegram stickers to WhatsApp (requires [Msb App](https://github.com/star-39/msb_app), supports iPhone and Android).
* Manage your sticker set interactively through WebApp: add/move/remove/edit sticker and emoji.
* Provides a CLI app [msbimport](https://github.com/star-39/moe-sticker-bot/tree/master/pkg/msbimport) for downloading LINE/Kakaotalk stickers.
* 輕鬆匯入LINE/kakao貼圖包到Telegram, 可以統一或分開指定emoji.
* 輕鬆使用自己任意格式的圖片和影片來創建自己的貼圖包或表情貼.
* 支援混合貼圖包。動態靜態貼圖可以放在同一個包內。
* 下載Telegram/LINE/kakao貼圖包和GIF, 自動變換為常用格式, 並且保留原檔.
* 匯出Telegram的貼圖包至WhatsApp(需要安裝[Msb App](https://github.com/star-39/msb_app), 支援iPhone和Android)。
* 互動式WebApp可以輕鬆管理自己的貼圖包: 可以新增/刪除貼圖, 移動位置或修改emoji.
* 提供名為[msbimport](https://github.com/star-39/moe-sticker-bot/tree/master/pkg/msbimport)的終端機程式, 用於下載LINE/kakao貼圖。
## Screenshots
[](https://t.me/moe_sticker_bot)
## Deployment
### Deploy with pre-built containers
It is recommended to deploy moe-sticker-bot using containers.
A pre-built OCI container is available at https://github.com/users/star-39/packages/container/package/moe-sticker-bot
Run:
```
docker run -dt ghcr.io/star-39/moe-sticker-bot /moe-sticker-bot --bot_token="..."
```
If you are on ARM64 machine, use `aarch64` tag.
See a real world deployment example on [deployments/kubernetes_msb.yaml](https://github.com/star-39/moe-sticker-bot/blob/master/deployments/kubernetes_msb.yaml).
### System Dependencies
* ImageMagick(6 or 7, both fine)
* bsdtar (libarchive-tools)
* ffmpeg (Requires a lot of components, use a fat static build or package manager's ones pls!)
* curl
* gifsicle (for converting GIF)
* python3 (for following tools)
* [msb_emoji.py](https://github.com/star-39/moe-sticker-bot/tree/master/tools/msb_emoji.py) (for extracting emoji)
* [msb_kakao_decrypt.py](https://github.com/star-39/moe-sticker-bot/tree/master/tools/msb_kakao_decrypt.py) (for decrypting animated kakao)
* [msb_rlottie.py](https://github.com/star-39/moe-sticker-bot/tree/master/tools/msb_rlottie.py) (for converting TGS)
* mariadb-server (optional, for database)
* nginx (optional, for WebApp)
Dependencies above must be accessible through PATH. Don't ask me why they are reported missing by bot.
## Build
### Build Dependencies
* golang v1.18+
* nodejs v18+ (optional, for WebApp)
* react-js v18+ (optional, for WebApp)
```bash
git clone https://github.com/star-39/moe-sticker-bot && cd moe-sticker-bot
go build -o moe-sticker-bot cmd/moe-sticker-bot/main.go
```
#### WebApp
Since 2.0 version of moe-sticker-bot, managing sticker set's order and emoji is achieved using Telegram's new WebApp technology.
See details on [web/webapp](https://github.com/star-39/moe-sticker-bot/tree/master/web/webapp3)
## CHANGELOG
v2.5.0-RC1(20240531)
* Support mix-typed sticker set.
* You can add video to static set and vice versa.
* Removed WhatsApp export temporarily .
* Many bug fixes.
v2.4.0-RC3(20240226)
* Remove deduplication during database curation.
* LINE emoji can be imported to either sticker or CustomEmoji.
* Support creating sticker set with under 50 stickers in batch.
* Import speed is imcreased significantly.
v2.4.0-RC1-RC2(20240207)
* Support Importing LINE Emoji into CustomEmoji.
* Support creating CustomEmoji.
* Support editing sticker emoji and title.
v2.3.14-2.3.15(20230228)
* Fix missing libwebp in OCI.
* Support TGS export.
* Improve GIF efficiency.
v2.3.11-v2.3.13 (20230227)
* Fix flood limit by ignoring it.
* Fix managing video stickers.
* Improved WhatsApp export.
* Support region locked LINE Message sticker.
* 修復flood limit匯入錯誤
* 修復動態貼圖管理。
* 改善WhatsApp匯出。
* 支援有區域鎖的line訊息貼圖。
v2.3.8-2.3.10 (20230217)
* Fix kakao import fatal, support more animated kakao.
* 修復KAKAO匯入錯誤, 支援更多KAKAO動態貼圖.
* Fix static webp might oversize(256KB)
* Fix panic during assign: invalid sticker emojis
* Fix some kakao animated treated as static
* Improved kakao static sticker quality
* Improved user experience
v2.3.6-2.3.7 (20230213)
* Support webhook.
* Support animated webp for user sticker.
* Add change sticker set title "feature"
* Fix "sticker order mismatch" when using WebApp to sort.
* Fix error on emoji assign.
* Fix too large animated kakao sticker.
v2.3.1-2.3.5 (20230209)
* Fix i18n titles.
* Fix flood limit by implementing channel to limit autocommit cocurrency.
* Fix error on webapp
* Fix import hang
* Fix fatal error not being reported to user.
* Fix typos
v2.3.0 (20230207)
* Fix flood limit by using local api server.
* Support webhook and local api server.
* Huge performance gain.
* Fix /search panic.
v2.2.1 (20230204)
* Fix webm too big.
* Fix id too long.
* Tuned flood limit algorithm.
v2.2.0 (20230131)
* Support animated kakao sticker.
* 支援動態kakao貼圖。
v2.1.0 (20230129)
* Support exporting sticker to WhatsApp.
* 支援匯出貼圖到WhatsApp
2.0.1 (20230106)
* Fix special LINE officialaccount sticker.
* Fix `--log_level` cmdline parsing.
* Thank you all! This project has reached 100 stars!
2.0.0 (20230105)
* Use new WebApp from /manage command to edit sticker set with ease.
* Send text or use /search command to search imported LINE/kakao sticker sets by all users.
* Auto import now happens on backgroud.
* Downloading sticker set is now lot faster.
* Fix many LINE import issues.
* 通過/manage指令使用新的WebApp輕鬆管理貼圖包.
* 直接傳送文字或使用/search指令來搜尋所有用戶匯入的LINE/KAKAO貼圖包.
* 自動匯入現在會在背景處理.
* 下載整個貼圖包的速度現在會快許多.
* 修復了許多LINE貼圖匯入的問題.
Detailed 2.0 Changelogs 詳細的2.0變更列表
2.0.0 (20230104)
* Improve flood limit handling.
* Auto LINE import now happens on backgroud.
* Improve GIF download.
2.0.0 RC-7 (20221230)
* Support /search in group chat.
* Fix search result.
* Fix empty sticker title.
* Sticker download is now parallel.
2.0.0 RC-6 (20221220)
* Fix line APNG with unexpected `tEXt` chunk.
* Changed length of webm from 2.9s to 3s.
* Minor improvements.
2.0.0 RC-5 (20221211)
* Fix potential panic when onError
* Warn user sticker set is full.
* Fix LINE message sticker with region lock.
2.0.0 RC-4 (20221211)
* Fix edit sticker on iOS
* Fix error editing multiple emojis.
2.0.0 RC-3 (20221210)
* Complies to LINE store's new UA requeirments.
* Fix animated sticker in webapp.
* Fixed sticker download
* Fixed webapp image aspect ratio.
2.0.0 RC-2 (20221210)
* Fix fatalpanic on webapp.
* Add /search functionality.
* Removed gin TLS support.
* Auto database curation.
2.0.0 RC-1 (20221206)
* WebApp support for edit stickers.
* Code structure refactored.
* Now accepts options from cmdline instead of env var.
* Support parallel sticker download.
* Fix LINE officialaccount/event/sticker
* Fix kakao link with queries.
1.2.4 (20221111)
* Minor improvements.
* Fixed(almost) flood limit.
* Fixed kakao link with queries.
1.2.2 (20220523)
* Improved user experience.
1.2.1 (20220520)
* Improved emoji edit.
1.2 (20220518)
* Fix import error for LINE ID < 775
* Improved UX during /import.
* Warn user if sticker set is already full.
1.1 (20220517)
* Code refactors.
* UX improvements.
* Skip error on TGS to GIF due to lottie issues.
1.0 (20220513)
* First stable release in go version.
* Added support for downloading TGS and convert to GIF.
* Backing database for @moe_sticker_bot has gone complete sanitization.
1.0 RC-9(20220512)
* Add an administrative command to _sanitize_ database, which purges duplicated stickers.
* Add an advanced command /register, to register your sticker to database.
* Minor bug fixes.
* This is the REAL final RC release, next one is stable!
1.0 RC-8 GO(20220512)
* Fix rand number in ID.
* Major code refactor.
* Downlaod sticker now happens on background.
* Better documentation.
* This release should be the final RC... hopefully.
1.0 RC-7 GO(20220511)
* You can specify custom ID when /create.
* Changed import ID naming scheme for cleaner look.
* Die immediately if FloodLimit exceeds 4.
* If everything looks good, this should be the last RC for 1.0
1.0 RC-6 GO(20220511)
* New feature: Change sticker order.
* New feature: Edit sticker emoji.
* New import support: kakaotalk emoticon.
* Fix possible panic when editMessage.
* We are closing to a stable release! RC should not exceed 8.
1.0 RC-5 GO(20220510)
* New feature: download raw line stickers to zip.
* FatalError now prints stack trace.
* zh-Hant is now default in auto LINE title.
* Quality of video sticker should improve by a bit.
* Fix possible slice out of range panic.
* If user experience FloodLimit over 3 times, terminate process.s
1.0 RC-4 GO(20220509)
* Use my custom fork of telebot
* User sent sticker now supports any file.
1.0 RC-3 GO(20220509)
* Split large zip to under 50MB chunks.
* Split long message to chunks.
* GIF downloaded is now in original 512px resolution.
* You can press "No" now when sent link or sticker.
* If error is not HTTP400, bot will retry for 3 times.
* Other minor improvements.
1.0 RC-2 GO(20220508)
* Fix SEGV when user requested /quit
* Ignore FloodLimit by default since API will do retry at TDLib level.
* Fix emoji in database.
* Fix video sticker when /manage.
* Support line message sticker and emoticon(emoji).
1.0 RC-1 GO(20220506)
* Completely rewritten whole project to golang
* Countless bug fixes.
* You can send sticker or link without a command now.
* Performance gained by a lot thanks to goroutine and worker pool.
Old changelogs
5.1 RC-4 (20220423)
* Fix duplicated sticker.
* Fix alpha channel converting GIF.
5.1 RC-3 (20220416)
* Do not use joblib due to their bugs.
* /download_telegram_sticker now converts to standard GIFs.
5.1 RC-2 (20220326)
* Significantly improved perf by using parallel loop.
* Sanitize kakao id starting with -
5.1 RC-1 (20220309)
* Support kakaotalk emoticon.
* Add more check for telegram sticker id.
5.0 RC-12 (20220305)
* Database now records line link and emoji settings.
* Fix issue when line name has <> marks.
* Fix issue adding video to static set.
* Fix hang handling CallbackQuery.
5.0 RC-11 (20220303)
* You can now delete a sticker set.
* /manage_sticker_set will show sets you created.
* Fix missing sticker during USER_STICKER.
5.0 RC-10 (20220226)
* Performance is now significantly improved.
* Fix issue converting Line message stickers.
* Bypass some regional block by LINE.
5.0 RC-9 (20220223)
* Splitted line popups to two categories, one keeping animated only.
* Bot now has a database stroing "good" imported sticker sets.
* Fix duplicated stickers in sticker set.
5.0 RC-8 (20220222)
* Fix user sticker parsing.
* Add support for MdIcoFlashAni_b
5.0 RC-7 (20220215)
* Fix exception if user sent nothing during USER_STICKER
* Fix a bug where /import_line_sticker may have no response.
* Corrected file download limit.
* Fix animated sticon
* Fix import hang due to missing ffmpeg '-y' param.
5.0 RC-6 (20220215)
* Fix python-telegram-bot WebHook problem.
* Fix emoji assign.
* Fix black background video sticker.
* Fix "Sticker too big" when uploading video sticker.
5.0 RC-5 (20220214)
* Allow using WebHook for better performance.
* Code refactors.
* Support Line name sticker.
5.0 RC-4 (20220212)
* Improved user sticker exprerience.
5.0 RC-3 (20220212)
* Fix a bug where creating sticker set with one sticker will cause fatal error.
* Fix missing clean_userdata in /download_line_sticker
* Tune VP9 params to avoid hitting 256K limit. This reduces video quality by a bit.
5.0 RC-2 (20220211)
* Fix media_group
* Minor bug fixes.
* Version 5.0 now enters freature freeze. No new feature will be added. Will have bug fixes only.
5.0 RC-1 (20220211)
* Support Line popup sticker without sound
* Support AVIF.
* Many bug fixes.
5.0 ALPHA-1 (20220211)
* Full support of animated(video) sticker. 完整支援動態貼圖. アニメーションスタンプフル対応。
* New feature: /manage_sticker_set, now you can add, delete, move sticker in a sticker set.
* Add support for Line full screen sticker(animated).
4.0 ALPHA-5 (20220210)
* Bring back fake RetryAfter check since some people still having this issue.
4.0 ALPHA-4 (20220210)
* Support user uploaded animated(video) stickers. You can both create or add to set.
* Better support sticon(line_emoji)
* Bug fixes.
4.0 ALPHA-3 (20220209)
* Supports all special line stickers,
* including effect_animation and sticon(emoji)
4.0 ALPHA-1 (20220209)
* Supports animated line sticker import.
## Special Thanks:
[](https://www.oracle.com/cloud/) for free 4CPU AArch64 Cloud Instance.
貼圖群 - Sticker Group Taiwan for testing and reporting.
[LINE Corp](https://linecorp.com/) / [Kakao Corp](http://www.kakaocorp.com/) for cute stickers.
https://github.com/blluv/KakaoTalkEmoticonDownloader MIT License Copyright @blluv
https://github.com/laggykiller/rlottie-python GPL-2.0 license Copyright @laggykiller
You and all the users! ☺
## License
The GPL V3 License

================================================
FILE: cmd/moe-sticker-bot/main.go
================================================
package main
import (
"flag"
"os"
"strings"
log "github.com/sirupsen/logrus"
"github.com/star-39/moe-sticker-bot/core"
)
// Common abbr. in this project:
// S : Sticker
// SS : StickerSet
func main() {
conf := parseCmdLine()
core.Init(conf)
}
func parseCmdLine() core.ConfigTemplate {
var help = flag.Bool("help", false, "Show help")
var adminUid = flag.Int64("admin_uid", -1, "Admin's UID(optional)")
var botToken = flag.String("bot_token", "", "Telegram Bot Token")
var dataDir = flag.String("data_dir", "", "Overwrites the working directory where msb puts data.")
var webappUrl = flag.String("webapp_url", "", "Public HTTPS URL to WebApp, in unset, webapp will be disabled.")
var WebappApiListenAddr = flag.String("webapp_listen_addr", "", "Webapp API server listen address(IP:PORT)")
var webappDataDir = flag.String("webapp_data_dir", "", "Where to put webapp data to share with ReactApp ")
var dbAddr = flag.String("db_addr", "", "mariadb(mysql) address, if unset, database will be disabled.")
var dbUser = flag.String("db_user", "", "mariadb(mysql) usernmae")
var dbPass = flag.String("db_pass", "", "mariadb(mysql) password")
var logLevel = flag.String("log_level", "debug", "Log level")
// var botApiAddr = flag.String("botapi_addr", "", "Local Bot API Server Address")
// var botApiDir = flag.String("botapi_dir", "", "Local Bot API Working directory")
// var webhookPublicAddr = flag.String("webhook_public_addr", "", "Webhook public address(WebhookEndpoint).")
// var webhookListenAddr = flag.String("webhook_listen_addr", "", "Webhook listen address(IP:PORT)")
// var webhookCert = flag.String("webhook_cert", "", "Certificate for WebHook")
flag.Parse()
if *help {
flag.Usage()
println("Only --bot_token is required to run.")
os.Exit(0)
}
conf := core.ConfigTemplate{}
conf.BotToken = *botToken
if conf.BotToken == "" {
log.Error("Please set --bot_token")
log.Error("Use --help to see options.")
log.Fatal("No bot token provided!")
}
if !strings.Contains(conf.BotToken, ":") {
log.Fatal("Bad bot token!")
}
conf.DbAddr = *dbAddr
conf.DbUser = *dbUser
conf.DbPass = *dbPass
conf.WebappUrl = *webappUrl
conf.WebappDataDir = *webappDataDir
conf.WebappApiListenAddr = *WebappApiListenAddr
// conf.BotApiAddr = *botApiAddr
// conf.BotApiDir = *botApiDir
// conf.WebhookPublicAddr = *webhookPublicAddr
// conf.WebhookListenAddr = *webhookListenAddr
// conf.WebhookCert = *webhookCert
conf.LogLevel = *logLevel
conf.AdminUid = *adminUid
conf.DataDir = *dataDir
return conf
// core.Config = conf
}
================================================
FILE: cmd/msbimport/main.go
================================================
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
log "github.com/sirupsen/logrus"
"github.com/star-39/moe-sticker-bot/pkg/msbimport"
)
func main() {
var link = flag.String("link", "", "Import link(LINE/kakao)")
var convertTG = flag.Bool("convert", false, "Convert to Telegram format(WEBP/WEBM)")
var outputJson = flag.Bool("json", false, "Output JSON serialized LineData, useful when integrating with other apps.")
var workDir = flag.String("dir", "", "Where to put sticker files.")
var logLevel = flag.String("log_level", "debug", "Log level")
flag.Parse()
if *outputJson {
log.SetLevel(log.FatalLevel)
} else {
ll, err := log.ParseLevel(*logLevel)
if err != nil {
log.SetLevel(log.DebugLevel)
} else {
log.SetLevel(ll)
}
}
msbimport.InitConvert()
ctx, _ := context.WithCancel(context.Background())
ld := &msbimport.LineData{}
// LineData will be parsed to ld.
warn, err := msbimport.ParseImportLink(*link, ld)
if err != nil {
log.Error("Error parsing import link!")
log.Fatalln(err)
}
if warn != "" {
log.Warnln(warn)
}
err = msbimport.PrepareImportStickers(ctx, ld, *workDir, *convertTG, false)
if err != nil {
log.Fatalln(err)
}
for _, lf := range ld.Files {
lf.Wg.Wait()
if lf.CError != nil {
log.Error(lf.CError)
}
log.Infoln("Original File:", lf.OriginalFile)
if *convertTG {
log.Infoln("Converted File:", lf.ConvertedFile)
}
}
if *outputJson {
ld.TitleWg.Wait()
jbytes, err := json.Marshal(ld)
if err != nil {
log.Fatalln(err)
}
fmt.Print(string(jbytes))
}
}
================================================
FILE: core/admin.go
================================================
package core
import (
tele "gopkg.in/telebot.v3"
)
func cmdSitRep(c tele.Context) error {
// Report status.
// stat := []string{}
// py_emoji_ok, _ := httpGet("http://127.0.0.1:5000/status")
// stat = append(stat, "py_emoji_ok? :"+py_emoji_ok)
// c.Send(strings.Join(stat, "\n"))
return nil
}
func cmdGetFID(c tele.Context) error {
initUserData(c, "getfid", "waitMFile")
if c.Message().Media() != nil {
return c.Reply(c.Message().Media().MediaFile().FileID)
} else {
return nil
}
}
================================================
FILE: core/commands.go
================================================
package core
import (
"strings"
log "github.com/sirupsen/logrus"
tele "gopkg.in/telebot.v3"
)
func cmdCreate(c tele.Context) error {
initUserData(c, "create", "waitSType")
return sendAskSTypeToCreate(c)
}
func cmdManage(c tele.Context) error {
err := sendUserOwnedS(c)
if err != nil {
return sendNoSToManage(c)
}
// V2.3: Do not init command on /manage
// initUserData(c, "manage", "waitSManage")
sendAskSToManage(c)
return nil
}
func cmdImport(c tele.Context) error {
// V2.2: Do not init command on /import
// initUserData(c, "import", "waitImportLink")
return sendAskImportLink(c)
}
func cmdDownload(c tele.Context) error {
// V2.2: Do not init command on /download
// initUserData(c, "download", "waitSDownload")
return sendAskWhatToDownload(c)
}
func cmdAbout(c tele.Context) error {
sendAboutMessage(c)
return nil
}
func cmdFAQ(c tele.Context) error {
sendFAQ(c)
return nil
}
func cmdPrivacy(c tele.Context) error {
return sendPrivacy(c)
}
func cmdChangelog(c tele.Context) error {
return sendChangelog(c)
}
func cmdStart(c tele.Context) error {
return sendStartMessage(c)
}
func cmdCommandList(c tele.Context) error {
return sendCommandList(c)
}
func cmdSearch(c tele.Context) error {
if c.Chat().Type == tele.ChatGroup || c.Chat().Type == tele.ChatSuperGroup {
return cmdGroupSearch(c)
}
initUserData(c, "search", "waitSearchKW")
return sendAskSearchKeyword(c)
}
func cmdGroupSearch(c tele.Context) error {
args := strings.Split(c.Text(), " ")
if len(args) < 2 {
return sendBadSearchKeyword(c)
}
keywords := args[1:]
lines := searchLineS(keywords)
if len(lines) == 0 {
return sendSearchNoResult(c)
}
return sendSearchResult(10, lines, c)
}
func cmdQuit(c tele.Context) error {
log.Debug("Received user quit request.")
ud, exist := users.data[c.Sender().ID]
if !exist {
return c.Send("Please use /start", &tele.ReplyMarkup{RemoveKeyboard: true})
}
c.Send("Please wait...")
ud.cancel()
// for _, s := range ud.stickerData.stickers {
// s.wg.Wait()
// }
terminateSession(c)
return nil
}
================================================
FILE: core/config.go
================================================
package core
var msbconf ConfigTemplate
type ConfigTemplate struct {
AdminUid int64
DataDir string
LogLevel string
// UseDB bool
BotToken string
// WebApp bool
WebappUrl string
WebappApiListenAddr string
WebappDataDir string
DbAddr string
DbUser string
DbPass string
// BotApiAddr string
// BotApiDir string
// WebhookPublicAddr string
// WebhookListenAddr string
// WebhookCert string
// WebhookSecretToken string
}
================================================
FILE: core/database.go
================================================
package core
import (
"database/sql"
"strings"
"github.com/go-sql-driver/mysql"
log "github.com/sirupsen/logrus"
)
/*
DATABASE VERSION 2 SCHEMA
MariaDB > show tables;
+----------------------------------+
| Tables_in_BOT_NAME_db |
+----------------------------------+
| line |
| properties |
| stickers |
+----------------------------------+
MariaDB > desc line;
+------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| line_id | varchar(128) | YES | | NULL | |
| tg_id | varchar(128) | YES | | NULL | |
| tg_title | varchar(255) | YES | | NULL | |
| line_link | varchar(512) | YES | | NULL | |
| auto_emoji | tinyint(1) | YES | | NULL | |
+------------+--------------+------+-----+---------+-------+
MariaDB > desc stickers;
+-----------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+-------+
| user_id | bigint(20) | YES | | NULL | |
| tg_id | varchar(128) | YES | | NULL | |
| tg_title | varchar(255) | YES | | NULL | |
| timestamp | bigint(20) | YES | | NULL | |
+-----------+--------------+------+-----+---------+-------+
MariaDB > desc properties;
+-------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| name | varchar(128) | NO | PRI | NULL | |
| value | varchar(128) | YES | | NULL | |
+-------+--------------+------+-----+---------+-------+
Current entries for properties:
name: DB_VER
value: 2
name: last_line_dedup_index
value: -1
*/
var db *sql.DB
const DB_VER = "2"
func initDB(dbname string) error {
addr := msbconf.DbAddr
user := msbconf.DbUser
pass := msbconf.DbPass
params := make(map[string]string)
params["autocommit"] = "1"
dsn := &mysql.Config{
User: user,
Passwd: pass,
Net: "tcp",
Addr: addr,
AllowNativePasswords: true,
Params: params,
}
db, _ = sql.Open("mysql", dsn.FormatDSN())
err := verifyDB(dsn, dbname)
if err != nil {
return err
}
db.Close()
dsn.DBName = dbname
db, _ = sql.Open("mysql", dsn.FormatDSN())
log.Debugln("DB DSN:", dsn.FormatDSN())
var dbVer string
err = db.QueryRow("SELECT value FROM properties WHERE name=?", "DB_VER").Scan(&dbVer)
if err != nil {
log.Errorln("Error quering dbVer, database corrupt? :", err)
return err
}
log.Infoln("Queried dbVer is :", dbVer)
checkUpgradeDatabase(dbVer)
log.WithFields(log.Fields{"Addr": addr, "DBName": dbname}).Info("MariaDB OK.")
return nil
}
func verifyDB(dsn *mysql.Config, dbname string) error {
err := db.Ping()
if err != nil {
log.Errorln("Error connecting to mariadb!! DSN: ", dsn.FormatDSN())
return err
}
_, err = db.Exec("USE " + dbname)
if err != nil {
log.Infoln("Can't USE database!", err)
log.Infof("Database name:%s does not seem to exist, attempting to create.", dbname)
err2 := createMariadb(dsn, dbname)
if err2 != nil {
log.Errorln("Error creating mariadb database!! DSN:", dsn.FormatDSN())
return err2
}
}
return nil
}
func checkUpgradeDatabase(queriedDbVer string) {
if queriedDbVer == "1" {
db.Exec("INSERT properties (name, value) VALUES (?, ?)", "last_line_dedup_index", "-1") //value is string!
db.Exec("UPDATE properties SET value=? WHERE name=?", "2", "DB_VER")
log.Info("Upgraded DB_VER from 1 to 2")
}
}
func createMariadb(dsn *mysql.Config, dbname string) error {
_, err := db.Exec("CREATE DATABASE " + dbname + " CHARACTER SET utf8mb4")
if err != nil {
log.Errorln("Error CREATE DATABASE!", err)
return err
}
db.Close()
dsn.DBName = dbname
db, _ = sql.Open("mysql", dsn.FormatDSN())
db.Exec("CREATE TABLE line (line_id VARCHAR(128), tg_id VARCHAR(128), tg_title VARCHAR(255), line_link VARCHAR(512), auto_emoji BOOL)")
db.Exec("CREATE TABLE properties (name VARCHAR(128) PRIMARY KEY, value VARCHAR(128))")
db.Exec("CREATE TABLE stickers (user_id BIGINT, tg_id VARCHAR(128), tg_title VARCHAR(255), timestamp BIGINT)")
db.Exec("INSERT properties (name, value) VALUES (?, ?)", "last_line_dedup_index", "-1")
db.Exec("INSERT properties (name, value) VALUES (?, ?)", "DB_VER", DB_VER)
log.Infoln("Mariadb initialized with DB_VER :", DB_VER)
return nil
}
func insertLineS(lineID string, lineLink string, tgID string, tgTitle string, aE bool) {
if db == nil {
return
}
if lineID == "" || lineLink == "" || tgID == "" || tgTitle == "" {
log.Warn("Empty entry to insert line s")
return
}
_, err := db.Exec("INSERT line (line_id, line_link, tg_id, tg_title, auto_emoji) VALUES (?, ?, ?, ?, ?)",
lineID, lineLink, tgID, tgTitle, aE)
if err != nil {
log.Errorln("Failed to insert line s:", lineID, lineLink)
} else {
log.Infoln("Insert LineS OK ->", lineID, lineLink, tgID, tgTitle, aE)
}
}
func insertUserS(uid int64, tgID string, tgTitle string, timestamp int64) {
if db == nil {
return
}
if tgID == "" || tgTitle == "" {
log.Warn("Empty entry to insert user s")
return
}
_, err := db.Exec("INSERT stickers (user_id, tg_id, tg_title, timestamp) VALUES (?, ?, ?, ?)",
uid, tgID, tgTitle, timestamp)
if err != nil {
log.Errorln("Failed to insert user s:", tgID, tgTitle)
} else {
log.Infoln("Insert UserS OK ->", tgID, tgTitle, timestamp)
}
}
// Pass QUERY_ALL to select all rows.
func queryLineS(id string) []LineStickerQ {
if db == nil {
return nil
}
var qs *sql.Rows
var lines []LineStickerQ
var tgTitle string
var tgID string
var aE bool
if id == "QUERY_ALL" {
qs, _ = db.Query("SELECT tg_title, tg_id, auto_emoji FROM line")
} else {
qs, _ = db.Query("SELECT tg_title, tg_id, auto_emoji FROM line WHERE line_id=?", id)
}
defer qs.Close()
for qs.Next() {
err := qs.Scan(&tgTitle, &tgID, &aE)
if err != nil {
return nil
}
lines = append(lines, LineStickerQ{
Tg_id: tgID,
Tg_title: tgTitle,
Ae: aE,
})
log.Debugf("Matched line record: id:%s | title:%s | ae:%v", tgID, tgTitle, aE)
}
err := qs.Err()
if err != nil {
log.Errorln("error quering line db: ", id)
return nil
}
return lines
}
// Pass -1 to query all rows.
func queryUserS(uid int64) []UserStickerQ {
if db == nil {
return nil
}
var usq []UserStickerQ
var q *sql.Rows
var tgTitle string
var tgID string
var timestamp int64
if uid == -1 {
q, _ = db.Query("SELECT tg_title, tg_id, timestamp FROM stickers")
} else {
q, _ = db.Query("SELECT tg_title, tg_id, timestamp FROM stickers WHERE user_id=?", uid)
}
defer q.Close()
for q.Next() {
err := q.Scan(&tgTitle, &tgID, ×tamp)
if err != nil {
log.Errorln("error scanning user db all", err)
return nil
}
usq = append(usq, UserStickerQ{
tg_id: tgID,
tg_title: tgTitle,
timestamp: timestamp,
})
}
err := q.Err()
if err != nil {
log.Errorln("error quering all user S")
return nil
}
return usq
}
func matchUserS(uid int64, id string) bool {
if db == nil {
return false
}
//Allow admin to manage all sticker sets.
// if uid == msbconf.AdminUid {
// return true
// }
qs, _ := db.Query("SELECT * FROM stickers WHERE user_id=? AND tg_id=?", uid, id)
defer qs.Close()
return qs.Next()
}
func deleteUserS(tgID string) {
if db == nil {
return
}
_, err := db.Exec("DELETE FROM stickers WHERE tg_id=?", tgID)
if err != nil {
log.Errorln("Delete user s err:", err)
} else {
log.Infoln("Deleted from database for user sticker:", tgID)
}
}
func deleteLineS(tgID string) {
if db == nil {
return
}
_, err := db.Exec("DELETE FROM line WHERE tg_id=?", tgID)
if err != nil {
log.Errorln("Delete line s err:", err)
} else {
log.Infoln("Deleted from database for line sticker:", tgID)
}
}
func updateLineSAE(ae bool, tgID string) error {
if db == nil {
return nil
}
_, err := db.Exec("UPDATE line SET auto_emoji=? WHERE tg_id=?", ae, tgID)
return err
}
func searchLineS(keywords []string) []LineStickerQ {
if db == nil {
return nil
}
var statements []string
for _, s := range keywords {
statements = append(statements, "'%"+s+"%'")
}
statement := strings.Join(statements, " AND tg_title LIKE ")
log.Debugln("database: search statement:", statement)
qs, err := db.Query("SELECT tg_title, tg_id, auto_emoji FROM line WHERE tg_title LIKE " + statement)
if err != nil {
log.Warnln("db q err:", err)
return nil
}
var lines []LineStickerQ
var tgTitle string
var tgID string
var aE bool
defer qs.Close()
for qs.Next() {
err := qs.Scan(&tgTitle, &tgID, &aE)
if err != nil {
return nil
}
lines = append(lines, LineStickerQ{
Tg_id: tgID,
Tg_title: tgTitle,
Ae: aE,
})
log.Debugf("Search matched line record: id:%s | title:%s | ae:%v", tgID, tgTitle, aE)
}
err = qs.Err()
if err != nil {
log.Errorln("error searching line db: ", keywords)
return nil
}
return lines
}
func curateDatabase() error {
log.Info("Starting database curation...")
invalidSSCount := 0
//Line stickers.
ls := queryLineS("QUERY_ALL")
for _, l := range ls {
log.Debugf("Scanning:%s", l.Tg_id)
ss, err := b.StickerSet(l.Tg_id)
if err != nil {
if strings.Contains(err.Error(), "is invalid") {
log.Infof("SS:%s is invalid. purging it from db...", l.Tg_id)
invalidSSCount++
deleteLineS(l.Tg_id)
deleteUserS(l.Tg_id)
} else {
log.Errorln(err)
}
continue
}
for si := range ss.Stickers {
if si > 0 {
if ss.Stickers[si].Emoji != ss.Stickers[si-1].Emoji {
log.Debugln("Setting auto emoji to FALSE for ", l.Tg_id)
updateLineSAE(false, l.Tg_id)
}
}
}
}
//User stickers.
us := queryUserS(-1)
for _, u := range us {
log.Debugf("Checking:%s", u.tg_id)
_, err := b.StickerSet(u.tg_id)
if err != nil {
if strings.Contains(err.Error(), "is invalid") {
log.Warnf("SS:%s is invalid. purging it from db...", u.tg_id)
deleteUserS(u.tg_id)
} else {
log.Errorln(err)
}
}
}
log.Infof("Database curation done. invalid:%d", invalidSSCount)
return nil
}
// func setLastLineDedupIndex(index int) {
// value := strconv.Itoa(index)
// db.Exec("UPDATE properties SET value=? WHERE name=?", value, "last_line_dedup_index")
// log.Infoln("setLastLineDedupIndex to :", value)
// }
// func getLastLineDedupIndex() int {
// var value string
// db.QueryRow("SELECT value FROM properties WHERE name=?", "last_line_dedup_index").Scan(&value)
// index, _ := strconv.Atoi(value)
// log.Infoln("getLastLineDedupIndex", value)
// return index
// }
================================================
FILE: core/define.go
================================================
package core
import (
"context"
"sync"
"github.com/go-co-op/gocron"
"github.com/panjf2000/ants/v2"
"github.com/star-39/moe-sticker-bot/pkg/msbimport"
tele "gopkg.in/telebot.v3"
)
var BOT_VERSION = "2.5.0-RC1"
var b *tele.Bot
var cronScheduler *gocron.Scheduler
var dataDir string
var botName string
// ['uid'] -> bool channels
var autocommitWorkersList = make(map[int64][]chan bool)
var users Users
var MSB_DEFAULT_STICKER_KEYWORDS = []string{"sticker", "moe_sticker_bot", "moe"}
const (
CB_DN_WHOLE = "dall"
CB_DN_SINGLE = "dsingle"
CB_OK_IMPORT = "yesimport"
CB_OK_IMPORT_EMOJI = "yesimportemoji"
CB_OK_DN = "yesd"
CB_BYE = "bye"
CB_MANAGE = "manage"
CB_DONE_ADDING = "done"
CB_YES = "yes"
CB_NO = "no"
CB_DEFAULT_TITLE = "titledefault"
CB_EXPORT_WA = "exportwa"
CB_ADD_STICKER = "adds"
CB_DELETE_STICKER = "dels"
CB_DELETE_STICKER_SET = "delss"
CB_CHANGE_TITLE = "changetitle"
CB_REGULAR_STICKER = "regular"
CB_CUSTOM_EMOJI = "customemoji"
ST_WAIT_WEBAPP = "waitWebApp"
ST_PROCESSING = "process"
FID_KAKAO_SHARE_LINK = "AgACAgEAAxkBAAEjezVj3_YXwaQ8DM-107IzlLSaXyG6yAACfKsxG3z7wEadGGF_gJrcnAEAAwIAA3kAAy4E"
LINK_TG = "t.me"
LINK_LINE = "line.me"
LINK_KAKAO = "kakao.com"
LINK_IMPORT = "IMPORT"
)
// Object for quering database for Line Sticker.
type LineStickerQ struct {
Line_id string
Line_link string
Tg_id string
Tg_title string
Ae bool
}
// Object for quering database for User Sticker.
type UserStickerQ struct {
tg_id string
tg_title string
timestamp int64
}
// Telegram API JSON.
type WebAppUser struct {
Id int
Is_bot bool
First_name string
Last_name string
Username string
Language_code string
Is_premium bool
Photo_url string
}
// Unique user data for one user and one session.
type UserData struct {
//waitgroup for sticker set, wait before commit.
wg sync.WaitGroup
//commit channel for emoji assign
commitChans []chan bool
ctx context.Context
cancel context.CancelFunc
//Current conversational state.
state string
sessionID string
workDir string
//Current command.
command string
progress string
progressMsg *tele.Message
lineData *msbimport.LineData
stickerData *StickerData
webAppUser *WebAppUser
webAppQID string
webAppWorkerPool *ants.PoolWithFunc
lastContext tele.Context
}
// Map for users, identified by user id.
// All temporal user data are stored in this struct.
type Users struct {
mu sync.Mutex
data map[int64]*UserData
}
// Object for ants worker function.
// wg must be initiated with wg.Add(1)
type StickerMoveObject struct {
wg sync.WaitGroup
err error
sd *StickerData
oldIndex int
newIndex int
}
func (ud *UserData) udSetState(state string) {
ud.state = state
}
// StickerFile object, for internal use.
// Also used for ants worker function.
// wg must be initialized with wg.Add(1) and must be waited when cPath is needed!
type StickerFile struct {
wg sync.WaitGroup
//Telegram FileID(if exists on cloud)
fileID string
// path of original file
// If fileID exists, oPath can be omitted.
oPath string
// path of converted filea
cPath string
cError error
//////////////////
//Following fields comply with tele.InputSticker
//////////////////
emojis []string `json:"emoji_list"`
keywords []string `json:"keywords"`
//One of static, video, animated.
format string `json:"format"`
}
// General sticker data for internal use.
type StickerData struct {
id string
// link string
title string
emojis []string
stickers []*StickerFile
stickerSet *tele.StickerSet
sDnObjects []*StickerDownloadObject
stickerSetType tele.StickerSetType
//either static or video, used for CreateNewStickerSet
// getFormat StickerData
isVideo bool
isAnimated bool
isCustomEmoji bool
pos int
// amount of local files
lAmount int
// amount on cloud
cAmount int
// amout of flood error encounterd
flCount int
}
type StickerDownloadObject struct {
wg sync.WaitGroup
sticker tele.Sticker
dest string
isVideo bool
//Convert to conventional format?
needConvert bool
//Shrink oversized GIF?
shrinkGif bool
//need to convert to WebApp use case
forWebApp bool
//need to convert to WhatsApp format
forWhatsApp bool
//need 96px PNG thumb for WhatsApp
forWhatsAppThumb bool
/*
Following fields are yielded by worker after wg is done.
*/
//Original sticker file downloaded.
of string
//Converted sticker file.
cf string
//Returned error.
err error
}
================================================
FILE: core/init.go
================================================
package core
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime/debug"
"time"
"github.com/go-co-op/gocron"
log "github.com/sirupsen/logrus"
"github.com/star-39/moe-sticker-bot/pkg/msbimport"
tele "gopkg.in/telebot.v3"
)
func Init(conf ConfigTemplate) {
msbconf = conf
initLogrus()
msbimport.InitConvert()
b = initBot(conf)
initWorkspace(b)
initWorkersPool()
go initGoCron()
if msbconf.WebappUrl != "" {
InitWebAppServer()
} else {
log.Info("WebApp not enabled.")
}
log.WithFields(log.Fields{"botName": botName, "dataDir": dataDir}).Info("Bot OK.")
// complies to telebot v3.1
b.Use(Recover())
b.Handle("/quit", cmdQuit)
b.Handle("/cancel", cmdQuit)
b.Handle("/exit", cmdQuit)
b.Handle("/faq", cmdFAQ)
b.Handle("/changelog", cmdChangelog)
b.Handle("/privacy", cmdPrivacy)
b.Handle("/help", cmdStart)
b.Handle("/about", cmdAbout)
b.Handle("/command_list", cmdCommandList)
b.Handle("/import", cmdImport, checkState)
b.Handle("/download", cmdDownload, checkState)
b.Handle("/create", cmdCreate, checkState)
b.Handle("/manage", cmdManage, checkState)
b.Handle("/search", cmdSearch, checkState)
// b.Handle("/register", cmdRegister, checkState)
b.Handle("/sitrep", cmdSitRep, checkState)
b.Handle("/getfid", cmdGetFID, checkState)
b.Handle("/start", cmdStart, checkState)
b.Handle(tele.OnText, handleMessage)
b.Handle(tele.OnVideo, handleMessage)
b.Handle(tele.OnAnimation, handleMessage)
b.Handle(tele.OnSticker, handleMessage)
b.Handle(tele.OnDocument, handleMessage)
b.Handle(tele.OnPhoto, handleMessage)
b.Handle(tele.OnCallback, handleMessage, autoRespond, sanitizeCallback)
b.Start()
}
// Recover returns a middleware that recovers a panic happened in
// the handler.
func Recover(onError ...func(error)) tele.MiddlewareFunc {
return func(next tele.HandlerFunc) tele.HandlerFunc {
return func(c tele.Context) error {
var f func(error)
if len(onError) > 0 {
f = onError[0]
} else {
f = func(err error) {
c.Bot().OnError(err, c)
}
}
defer func() {
if r := recover(); r != nil {
if err, ok := r.(error); ok {
f(err)
} else if s, ok := r.(string); ok {
f(errors.New(s))
}
}
}()
return next(c)
}
}
}
// This one never say goodbye.
func endSession(c tele.Context) {
cleanUserDataAndDir(c.Sender().ID)
}
// This one will say goodbye.
func terminateSession(c tele.Context) {
cleanUserDataAndDir(c.Sender().ID)
c.Send("Bye. /start")
}
func endManageSession(c tele.Context) {
ud, exist := users.data[c.Sender().ID]
if !exist {
return
}
if ud.stickerData.id == "" {
return
}
path := filepath.Join(msbconf.WebappDataDir, ud.stickerData.id)
os.RemoveAll(path)
}
func onError(err error, c tele.Context) {
log.Error("User encountered fatal error!")
log.Errorln("Raw error:", err)
log.Errorln(string(debug.Stack()))
defer func() {
if r := recover(); r != nil {
log.Errorln("Recovered from onError!!", r)
}
}()
if c == nil {
return
}
sendFatalError(err, c)
cleanUserDataAndDir(c.Sender().ID)
}
func initBot(conf ConfigTemplate) *tele.Bot {
var poller tele.Poller
url := tele.DefaultApiURL
pref := tele.Settings{
URL: url,
Token: msbconf.BotToken,
Poller: poller,
Synchronous: false,
// Genrally, issues are tackled inside each state, only fatal error should be returned to framework.
// onError will terminate current session and log to terminal.
OnError: onError,
}
log.WithField("token", msbconf.BotToken).Info("Attempting to initialize...")
b, err := tele.NewBot(pref)
if err != nil {
log.Fatal(err)
}
return b
}
func initWorkspace(b *tele.Bot) {
botName = b.Me.Username
if msbconf.DataDir != "" {
dataDir = msbconf.DataDir
} else {
dataDir = botName + "_data"
}
users = Users{data: make(map[int64]*UserData)}
err := os.MkdirAll(dataDir, 0755)
if err != nil {
log.Fatal(err)
}
if msbconf.DbAddr != "" {
dbName := botName + "_db"
err = initDB(dbName)
if err != nil {
log.Fatalln("Error initializing database!!", err)
}
} else {
log.Warn("Database not enabled because --db_addr is not set.")
}
}
// This gocron is intended to do periodic cleanups.
func initGoCron() {
// Delay start.
time.Sleep(15 * time.Second)
cronScheduler = gocron.NewScheduler(time.UTC)
cronScheduler.Every(1).Days().Do(purgeOutdatedStorageData)
if msbconf.DbAddr != "" {
cronScheduler.Every(1).Weeks().Do(curateDatabase)
}
cronScheduler.StartBlocking()
}
func initLogrus() {
log.SetFormatter(&log.TextFormatter{
ForceColors: true,
DisableLevelTruncation: true,
})
level, err := log.ParseLevel(msbconf.LogLevel)
if err != nil {
println("Error parsing log_level! Defaulting to DEBUG level.\n")
log.SetLevel(log.DebugLevel)
}
log.SetLevel(level)
fmt.Printf("Log level is set to: %s\n", log.GetLevel())
log.Debug("Warning: Log level below DEBUG might print sensitive information, including passwords.")
}
================================================
FILE: core/message.go
================================================
package core
import (
"errors"
"fmt"
"net/url"
"path/filepath"
"runtime/debug"
"strconv"
"strings"
"time"
log "github.com/sirupsen/logrus"
tele "gopkg.in/telebot.v3"
)
func sendStartMessage(c tele.Context) error {
message := `
Hi! I'm moe_sticker_bot! Please:
• Send LINE/Kakao sticker share link to import or download.
• Send Telegram sticker/link/GIF to download or export to WhatsApp.
• Send keywords to search sticker sets.
• Tap to /create or /manage sticker set and CustomEmoji.
• Tap to check all available /command_list.
你好! 歡迎使用萌萌貼圖BOT! 請:
• 傳送LINE/kakao貼圖包的分享連結來匯入或下載.
• 傳送Telegram貼圖/連結/GIF來下載.
• 傳送關鍵字來搜尋貼圖包.
• 傳送 /create 或 /manage 來創建或管理貼圖包和表情貼。
• 傳送 /command_list 檢視所有可用指令.
`
return c.Send(message, tele.ModeHTML, tele.NoPreview)
}
func sendCommandList(c tele.Context) error {
message := `
/import/search LINE/Kakao stickers.
匯入或搜尋LINE/Kaka貼圖包./download/create/manage Telegram stickers.
下載、創建、管理Telegram貼圖包./faq /about /changelog /privacy
常見問題/關於/更新紀錄/私隱
`
return c.Send(message, tele.ModeHTML, tele.NoPreview)
}
func sendAboutMessage(c tele.Context) {
c.Send(fmt.Sprintf(`
Please star for this project on Github if you like this bot!
如果您喜歡這個bot, 歡迎在Github給本專案標Star喔!
https://github.com/star-39/moe-sticker-bot
Thank you @StickerGroup for feedbacks and advices!
This free(as in freedom) software is released under the GPLv3 License.
Comes with ABSOLUTELY NO WARRANTY! All rights reserved.
本BOT為免費提供的自由軟體, 您可以自由使用/分發, 惟無任何保用(warranty)!
本軟體授權於通用公眾授權條款(GPL)v3, 保留所有權利.
Please send /start to start using
請傳送 /start 來開始
Version:版本: %s
`, BOT_VERSION), tele.ModeHTML)
}
func sendFAQ(c tele.Context) {
c.Send(fmt.Sprintf(`
Please hit Star for this project on Github if you like this bot!
如果您喜歡這個bot, 歡迎在Github給本專案標Star喔!
https://github.com/star-39/moe-sticker-bot
------------------------------------
Q: I got stucked! I can't quit from command!
我卡住了! 我沒辦法從指令中退出!
A: Please send /quit to interrupt.
請傳送 /quit 來中斷.
Q: Why ID has suffix: _by_%s ?
為甚麼ID的末尾有: _by_%s ?
A: It's forced by Telegram, bot created sticker set must have its name in ID suffix.
因為這個是Telegram的強制要求, 由bot創造的貼圖ID末尾必須有bot名字.
Q: Who owns the sticker sets the bot created?
BOT創造的貼圖包由誰所有?
A: It's you of course. You can manage them through /manage or Telegram's official @Stickers bot.
當然是您. 您可以通過 /manage 指令或者Telegram官方的 @Stickers 管理您的貼圖包.
`, botName, botName), tele.ModeHTML)
}
func sendChangelog(c tele.Context) error {
return c.Send(`
Details: 詳細:
https://github.com/star-39/moe-sticker-bot#changelog
v2.5.0-RC1(20240528)
* Support mix-typed sticker set.
* You can add video to static set and vice versa.
* Removed WhatsApp export temporarily .
* 支援混合貼圖包。
* 貼圖包可以同時存入靜態與動態貼圖。
* 暫時移除WhatsApp匯出功能。
v2.4.0-RC1-RC4(20240304)
* Support Importing LINE Emoji into CustomEmoji.
* Support creating CustomEmoji.
* Support editing sticker emoji and title.
* 支援LINE表情貼匯入。
* 支援創建表情貼。
* 支援修改貼圖Emoji/貼圖包標題。
v2.3.13-v2.3.15(20230228)
* Support region locked LINE Message sticker.
* Support TGS(Animated) sticker export.
* Fix TGS(Animated) sticker download.
* 支援有區域鎖的line訊息貼圖。
* 支援TGS貼圖匯出。
* 修復TGS(動態)貼圖下載問題.
v2.3.10(20230217)
* Fix kakao import fatal, support more animated kakao.
* 修復KAKAO匯入錯誤, 支援更多KAKAO動態貼圖.
v2.3.x (20230216)
* Fix flood limit error during import.
* Fix animated kakao treated as static.
* Improved static kakao quality.
* Support changing sticker title.
* 修復匯入貼圖時flood limit錯誤。
* 修復動態KAKAO被當作靜態.
* 提升靜態KAKAO畫質.
* 支援修改貼圖包標題.
v2.2.0 (20230131)
* Support animated kakao sticker.
* 支援動態kakao貼圖。
v2.1.0 (20230129)
* Support exporting sticker to WhatsApp.
* 支援匯出貼圖到WhatsApp
v2.0.0 (20230105)
* Use new WebApp from /manage command to edit sticker set with ease.
* Send text or use /search command to search imported LINE/kakao sticker sets by all users.
* Auto import now happens on backgroud.
* Downloading sticker set is now lot faster.
* Fix many LINE import issues.
* 通過 /manage 指令使用新的WebApp輕鬆管理貼圖包.
* 直接傳送文字或使用 /search 指令來搜尋所有用戶匯入的LINE/KAKAO貼圖包.
* 自動匯入現在會在背景處理.
* 下載整個貼圖包的速度現在會快許多.
* 修復了許多LINE貼圖匯入的問題.
`, tele.NoPreview)
}
func sendPrivacy(c tele.Context) error {
return c.Send(`
Privacy Notice:
None of your usage or behaviour will be stored or analyzed.
None of your user identifier or information will be collected or stored if you did not use /import or /create command and succeded.
If you used /create or /import feature and upon success,
your user identifier will be associated to the sticker set you create and stored to database on the bot server,
which will only be used to tell which sticker set is owned by you only when you use /manage command.
No one else could see or use the stored user identifier.
All the data being stored is encrypted.
This bot will never share any of the stored data to anyone or to anywhere else.
The bot server is physically located at Osaka,Japan. Local laws might apply.
This bot is free and open source software, you can see https://github.com/star-39/moe-sticker-bot/blob/master/core/database.go
to investigate how the bot store and process the stored data.
私隱聲明:
本bot不會存儲或分析您的使用情況或行為。
本bot不會採集或儲存任何用戶資訊,除非您使用了 /import 或 /create 指令且成功完成。
如果您使用了 /import 或 /create 指令並且成功完成,
您的用戶識別子(user_id)會與您創建的貼圖包關聯並存檔入bot伺服器的資料庫中。
此識別子只會用來讓您本人通過 /manage 指令查詢您所擁有的貼圖包,不作其他任何用途。
任何其他用戶無法看見或使用此識別子。
本bot儲存的所有資訊均經過加密。
本bot不會分享任何儲存的資訊給任何人或實體或到任何地方。
本bot伺服器的物理位置位於日本大阪。 當地法律可能適用。
本bot為自由開放原始碼軟體,請參閱 https://github.com/star-39/moe-sticker-bot/blob/master/core/database.go
來了解bot如何儲存和處理儲存的資訊。
`, tele.ModeHTML, tele.NoPreview)
}
func sendAskEmoji(c tele.Context) error {
selector := &tele.ReplyMarkup{}
btnManu := selector.Data("Assign separately/分別設定", "manual")
btnRand := selector.Data(`Batch assign as/一併設定為 "⭐"`, "random")
selector.Inline(selector.Row(btnManu), selector.Row(btnRand))
return c.Send(`
Telegram requires emoji to and keywords for each sticker:
• Press "Assign separately" to assign emoji and keywords one by one.
• Send an emoji to do batch assign.
Telegram要求為每張貼圖分別設定emoji和關鍵字:
• 按下"分別設定"來為每個貼圖分別設定相應的emoji和關鍵字.
• 傳送一個emoji來為全部貼圖設定成一樣的.
`, selector)
}
func sendConfirmExportToWA(c tele.Context, sn string, hex string) error {
selector := &tele.ReplyMarkup{}
baseUrl, _ := url.JoinPath(msbconf.WebappUrl, "export")
webAppUrl := fmt.Sprintf("%s?sn=%s&hex=%s", baseUrl, sn, hex)
log.Debugln("webapp export link is:", webAppUrl)
webapp := tele.WebApp{URL: webAppUrl}
btnExport := selector.WebApp("Continue export/繼續匯出 →", &webapp)
selector.Inline(selector.Row(btnExport))
return c.Reply(`
Exporting to WhatsApp requires Msb App due to their restrictions, then press "Continue export".
匯出到WhatsApp需要手機上安裝Msb App, 然後按下"繼續匯出".
Download:下載:
iPhone: AppStore(N/A.暫無), IPAAndroid: GooglePlay(N/A.暫無), APK
`, tele.ModeHTML, tele.NoPreview, selector)
}
func genSDnMnEInline(canManage bool, isTGS bool, sn string) *tele.ReplyMarkup {
selector := &tele.ReplyMarkup{}
btnSingle := selector.Data("Download this sticker/下載這張貼圖", CB_DN_SINGLE)
btnAll := selector.Data("Download sticker set/下載整個貼圖包", CB_DN_WHOLE)
btnMan := selector.Data("Manage sticker set/管理這個貼圖包", CB_MANAGE)
// btnExport := selector.Data("Export to WhatsApp/匯出到WhatsApp", CB_EXPORT_WA)
if canManage {
selector.Inline(selector.Row(btnSingle), selector.Row(btnAll), selector.Row(btnMan))
} else {
if isTGS {
//If is TGS, do not support export to WA.
selector.Inline(selector.Row(btnSingle), selector.Row(btnAll))
} else {
selector.Inline(selector.Row(btnSingle), selector.Row(btnAll))
}
}
return selector
}
func sendAskSDownloadChoice(c tele.Context, s *tele.Sticker) error {
selector := genSDnMnEInline(false, s.Animated, s.SetName)
return c.Reply(`
You can download this sticker or the whole sticker set, please select below.
您可以下載這個貼圖或者其所屬的整個貼圖包, 請選擇:
`, selector)
}
func sendAskSChoice(c tele.Context, sn string) error {
selector := genSDnMnEInline(true, false, sn)
return c.Reply(`
You own this sticker set. You can download or manage this sticker set, please select below.
您擁有這個貼圖包. 您可以下載或者管理這個貼圖包, 請選擇:
`, selector)
}
func sendAskTGLinkChoice(c tele.Context) error {
selector := &tele.ReplyMarkup{}
btnManu := selector.Data("Download sticker set/下載整個貼圖包", CB_DN_WHOLE)
btnMan := selector.Data("Manage sticker set/管理這個貼圖包", CB_MANAGE)
selector.Inline(selector.Row(btnManu), selector.Row(btnMan))
return c.Reply(`
You own this sticker set. You can download or manage this sticker set, please select below.
您擁有這個貼圖包. 您可以下載或者管理這個貼圖包, 請選擇:
`, selector)
}
func sendAskWantSDown(c tele.Context) error {
selector := &tele.ReplyMarkup{}
btn1 := selector.Data("Yes", CB_DN_WHOLE)
btnNo := selector.Data("No", CB_BYE)
selector.Inline(selector.Row(btn1), selector.Row(btnNo))
return c.Reply(`
You can download this sticker set. Press Yes to continue.
您可以下載這個貼圖包, 按下Yes來繼續.
`, selector)
}
func sendAskWantImportOrDownload(c tele.Context, avalAsEmoji bool) error {
msg := ""
selector := &tele.ReplyMarkup{}
btnImportSticker := selector.Data("Import as sticker set/作為普通貼圖包匯入", CB_OK_IMPORT)
btnImportEmoji := selector.Data("Import as CustomEmoji/作為表情貼匯入", CB_OK_IMPORT_EMOJI)
btnDownload := selector.Data("Download/下載", CB_OK_DN)
if avalAsEmoji {
selector.Inline(selector.Row(btnImportSticker), selector.Row(btnImportEmoji), selector.Row(btnDownload))
msg = `
You can import this sticker set to Telegram or download it.
Import as Custom Emoji is also available, however you will need Telegram Premium to send them.
您可以下載或匯入這個貼圖包到Telegram.
也可以作為表情貼匯入,但是傳送需要Telegram會員。`
} else {
selector.Inline(selector.Row(btnImportSticker), selector.Row(btnDownload))
msg = `
You can import this sticker set to Telegram or download it.
您可以下載或匯入這個貼圖包到Telegram.`
}
return c.Reply(msg, selector)
}
func sendAskWhatToDownload(c tele.Context) error {
return c.Send("Please send a sticker that you want to download, or its share link(can be either Telegram or LINE ones)\n" +
"請傳送想要下載的貼圖, 或者是貼圖包的分享連結(可以是Telegram或LINE連結).")
}
func sendAskTitle_Import(c tele.Context) error {
ld := users.data[c.Sender().ID].lineData
ld.TitleWg.Wait()
log.Debug("titles are::")
log.Debugln(ld.I18nTitles)
selector := &tele.ReplyMarkup{}
var titleButtons []tele.Row
var titleText string
for i, t := range ld.I18nTitles {
if t == "" {
continue
}
title := escapeTagMark(t) + " @" + botName
btn := selector.Data(title, strconv.Itoa(i))
row := selector.Row(btn)
titleButtons = append(titleButtons, row)
titleText = titleText + "\n" + title + ""
}
if len(titleButtons) == 0 {
btnDefault := selector.Data(escapeTagMark(ld.Title)+" @"+botName, CB_DEFAULT_TITLE)
titleButtons = []tele.Row{selector.Row(btnDefault)}
}
selector.Inline(titleButtons...)
return c.Send("Please send a title for this sticker set. You can also select an original title below:\n"+
"請傳送貼圖包的標題.您也可以按下面的按鈕自動填上合適的原版標題:\n"+
titleText, selector, tele.ModeHTML)
}
func sendAskTitle(c tele.Context) error {
return c.Send("Please send a title for this sticker set.\n" +
"請傳送貼圖包的標題.")
}
func sendAskID(c tele.Context) error {
selector := &tele.ReplyMarkup{}
btnAuto := selector.Data("Auto Generate/自動生成", "auto")
selector.Inline(selector.Row(btnAuto))
return c.Send(`
Please send an ID for sticker set, used in share link.
Can contain alphanum and underscore only.
請設定貼圖包的ID, 用於分享連結.
只可以含有英文,數字,下劃線.
For example: 例如:
My_favSticker21
ID is usually not important, you can press Auto Generate.
ID通常不重要, 您可以按下"自動生成".`, selector, tele.ModeHTML)
}
func sendAskImportLink(c tele.Context) error {
return c.Send(`
Please 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.
請傳送貼圖包的LINE/kakao Store連結. 您可以在App裡的貼圖商店按右上角的分享->複製連結來取得連結.
For example: 例如:
https://store.line.me/stickershop/product/7673/jahttps://e.kakao.com/t/pretty-all-friendshttps://emoticon.kakao.com/items/lV6K2fWmU7CpXlHcP9-ysQJx9rg=?referer=share_link
`, tele.ModeHTML)
}
func sendNotifySExist(c tele.Context, lineID string) bool {
lines := queryLineS(lineID)
if len(lines) == 0 {
return false
}
message := "This sticker set exists in our database, you can continue import or just use them if you want.\n" +
"此套貼圖包已經存在於資料庫中, 您可以繼續匯入, 或者使用下列現成的貼圖包\n\n"
var entries []string
for _, l := range lines {
if l.Ae {
entries = append(entries, fmt.Sprintf(`%s`, "https://t.me/addstickers/"+l.Tg_id, l.Tg_title))
} else {
// append to top
entries = append([]string{fmt.Sprintf(`★ %s`, "https://t.me/addstickers/"+l.Tg_id, l.Tg_title)}, entries...)
}
}
if len(entries) > 5 {
entries = entries[:5]
}
message += strings.Join(entries, "\n")
c.Send(message, tele.ModeHTML)
return true
}
func sendSearchResult(entriesWant int, lines []LineStickerQ, c tele.Context) error {
var entries []string
message := "Search Results: 搜尋結果:\n"
for _, l := range lines {
l.Tg_title = strings.TrimSuffix(l.Tg_title, " @"+botName)
if l.Ae {
entries = append(entries, fmt.Sprintf(`%s`, "https://t.me/addstickers/"+l.Tg_id, l.Tg_title))
} else {
// append to top
entries = append([]string{fmt.Sprintf(`★ %s`, "https://t.me/addstickers/"+l.Tg_id, l.Tg_title)}, entries...)
}
}
if entriesWant == -1 && len(entries) > 120 {
c.Send("Too many results, please narrow your keyword, truncated to 120 entries.\n" +
"搜尋結果過多,已縮減到120個,請使用更準確的搜尋關鍵字。")
entries = entries[:120]
}
if entriesWant != -1 && len(entries) > entriesWant {
entries = entries[:entriesWant]
}
if len(entries) > 30 {
eChunks := chunkSlice(entries, 30)
for _, eChunk := range eChunks {
msgToSend := message + strings.Join(eChunk, "\n")
c.Send(msgToSend, tele.ModeHTML)
}
} else {
message += strings.Join(entries, "\n")
c.Send(message, tele.ModeHTML)
}
return nil
}
func sendAskStickerFile(c tele.Context) error {
return c.Send("Please send images/photos/stickers(less than 120 in total),\n" +
"or send an archive containing image files,\n" +
"wait until upload complete, then tap 'Done adding'.\n\n" +
"請傳送任意格式的圖片/影片/貼圖(少於120張)\n" +
"或者傳送內有貼圖檔案的歸檔,\n" +
"等候所有檔案上載完成, 然後按下「停止增添」\n")
}
func sendInStateWarning(c tele.Context) error {
command := users.data[c.Sender().ID].command
state := users.data[c.Sender().ID].state
return c.Send(fmt.Sprintf(`
Please send content according to instructions.
請按照bot提示傳送相應內容.
Current command: %s
Current state: %s
You can also send /quit to terminate session.
您也可以傳送 /quit 來中斷對話.
`, command, state))
}
func sendNoSessionWarning(c tele.Context) error {
return c.Send("Please use /start or send LINE/kakao/Telegram links or stickers.\n請使用 /start 或者傳送LINE/kakao/Telegram連結或貼圖.")
}
func sendAskSTypeToCreate(c tele.Context) error {
selector := &tele.ReplyMarkup{}
btnRegular := selector.Data("Regular sticker set/普通貼圖包", CB_REGULAR_STICKER)
btnCustomEmoji := selector.Data("Custom Emoji/表情貼", CB_CUSTOM_EMOJI)
selector.Inline(selector.Row(btnRegular), selector.Row(btnCustomEmoji))
return c.Send("What kind of sticker set you want to create?\nNote that custom emoji can only be sent by Telegram Premium member."+
"您想要創建何種類型的貼圖包?\n請注意只有Telgram會員可以傳送表情貼。", selector)
}
func sendAskEmojiAssign(c tele.Context) error {
sd := users.data[c.Sender().ID].stickerData
sf := sd.stickers[sd.pos]
sf.wg.Wait()
caption := fmt.Sprintf(`
Send emoji(s) representing this sticker.
請傳送代表這個貼圖的emoji(可以多個).
%d of %d
`, sd.pos+1, sd.lAmount)
if sf.fileID != "" {
msg, _ := c.Bot().Send(c.Sender(), &tele.Sticker{
File: tele.File{FileID: sf.fileID},
})
_, err := c.Bot().Reply(msg, caption)
return err
}
err := c.Send(&tele.Video{
File: tele.FromDisk(sf.oPath),
Caption: caption,
})
if err != nil {
err2 := c.Send(&tele.Video{
File: tele.FromDisk(sd.stickers[sd.pos].oPath),
Caption: caption,
})
if err2 != nil {
err3 := c.Send(&tele.Document{
File: tele.FromDisk(sd.stickers[sd.pos].oPath),
FileName: filepath.Base(sd.stickers[sd.pos].oPath),
Caption: caption,
})
if err3 != nil {
err4 := c.Send(&tele.Sticker{File: tele.File{FileID: sd.stickers[sd.pos].oPath}})
if err4 != nil {
return err4
}
}
}
}
return nil
}
func sendFatalError(err error, c tele.Context) {
if c == nil {
return
}
var errMsg string
if err != nil {
errMsg = err.Error()
errMsg = strings.ReplaceAll(errMsg, msbconf.BotToken, "***")
if strings.Contains(errMsg, "500") {
errMsg += "\nThis is an internal error of Telegram server, we could do nothing but wait for its recover. Please try again later.\n" +
"此錯誤為Telegram伺服器之內部錯誤, 無法由bot解決, 只能等候官方修復. 建議您稍後再嘗試一次.\n"
}
}
c.Send("Fatal error encounterd. Please try again. /start\n"+
"發生嚴重錯誤. 請您從頭再試一次. /start \n\n"+
"You can report this error to https://github.com/star-39/moe-sticker-bot/issues\n\n"+
""+errMsg+"", tele.ModeHTML, tele.NoPreview)
}
func sendExecEmojiAssignFinished(c tele.Context) error {
ud := users.data[c.Sender().ID]
msg := fmt.Sprintf(`
LINE Cat: %s
LINE ID: %s
TG ID: %s
TG Title: %s
Success. 成功完成. /start
`, ud.lineData.Category,
ud.lineData.Id,
ud.stickerData.id,
"https://t.me/addstickers/"+ud.stickerData.id,
escapeTagMark(ud.stickerData.title),
)
return c.Send(msg, tele.ModeHTML)
}
// Return:
// string: Text of the message.
// *tele.Message: The pointer of the message.
// error: error
func sendProcessStarted(ud *UserData, c tele.Context, optMsg string) (string, *tele.Message, error) {
message := fmt.Sprintf(`
Preparing stickers, please wait...
正在準備貼圖, 請稍後...
LINE Cat: %s
LINE ID: %s
TG ID: %s
TG TYPE: %s
TG Title: %sProgress / 進展%s
`, ud.lineData.Category,
ud.lineData.Id,
ud.stickerData.id,
ud.stickerData.stickerSetType,
"https://t.me/addstickers/"+ud.stickerData.id,
escapeTagMark(ud.stickerData.title),
optMsg)
ud.progress = message
teleMsg, err := c.Bot().Send(c.Recipient(), message, tele.ModeHTML)
ud.progressMsg = teleMsg
return message, teleMsg, err
}
// if progressText is empty, a progress bar will be generated based on cur and total.
func editProgressMsg(cur int, total int, progressText string, originalText string, teleMsg *tele.Message, c tele.Context) error {
defer func() {
if r := recover(); r != nil {
log.Errorln("editProgressMsg encountered panic! ignoring...", string(debug.Stack()))
}
}()
header := originalText[:strings.LastIndex(originalText, "")]
prog := ""
if progressText != "" {
prog = progressText
goto SEND
}
cur = cur + 1
if cur == 1 {
prog = fmt.Sprintf("[=> ]\n %d of %d", cur, total)
} else if cur == int(float64(0.25)*float64(total)) {
prog = fmt.Sprintf("[====> ]\n %d of %d", cur, total)
} else if cur == int(float64(0.5)*float64(total)) {
prog = fmt.Sprintf("[=========> ]\n %d of %d", cur, total)
} else if cur == int(float64(0.75)*float64(total)) {
prog = fmt.Sprintf("[==============> ]\n %d of %d", cur, total)
} else if cur == total {
prog = fmt.Sprintf("[====================]\n %d of %d", cur, total)
} else {
return nil
}
SEND:
messageText := header + prog
c.Bot().Edit(teleMsg, messageText, tele.ModeHTML)
return nil
}
func sendAskSToManage(c tele.Context) error {
return c.Send("Send a sticker from the sticker set that want to edit,\n" +
"or send its share link.\n\n" +
"您想要修改哪個貼圖包? 請傳送那個貼圖包內任意一張貼圖,\n" +
"或者是它的分享連結.")
}
func sendUserOwnedS(c tele.Context) error {
usq := queryUserS(c.Sender().ID)
if usq == nil {
return errors.New("no sticker owned")
}
var entries []string
for _, us := range usq {
date := time.Unix(us.timestamp, 0).Format("2006-01-02 15:04")
title := strings.TrimSuffix(us.tg_title, " @"+botName)
//workaround for empty title.
if title == "" || title == " " {
title = "_"
}
entry := fmt.Sprintf(`%s`, us.tg_id, title)
entry += " | " + date
entries = append(entries, entry)
}
if len(entries) > 30 {
eChunks := chunkSlice(entries, 30)
for _, eChunk := range eChunks {
message := "You own following stickers:\n"
message += strings.Join(eChunk, "\n")
c.Send(message, tele.ModeHTML)
}
} else {
message := "You own following stickers:\n"
message += strings.Join(entries, "\n")
c.Send(message, tele.ModeHTML)
}
return nil
}
func sendAskEditChoice(c tele.Context) error {
ud := users.data[c.Sender().ID]
selector := &tele.ReplyMarkup{}
btnAdd := selector.Data("Add sticker/增添貼圖", CB_ADD_STICKER)
btnDel := selector.Data("Delete sticker/刪除貼圖", CB_DELETE_STICKER)
btnDelset := selector.Data("Delete sticker set/刪除貼圖包", CB_DELETE_STICKER_SET)
btnChangeTitle := selector.Data("Change title/修改標題", CB_CHANGE_TITLE)
btnExit := selector.Data("Exit/退出", "bye")
if msbconf.WebappUrl != "" {
baseUrl, _ := url.JoinPath(msbconf.WebappUrl, "edit")
url := fmt.Sprintf("%s?ss=%s&dt=%d",
baseUrl,
ud.stickerData.id,
time.Now().Unix())
log.Debugln("WebApp URL is : ", url)
webApp := &tele.WebApp{
URL: url,
}
btnEdit := selector.WebApp("Change order or emoji/修改順序或Emoji", webApp)
selector.Inline(
selector.Row(btnAdd), selector.Row(btnDel), selector.Row(btnDelset), selector.Row(btnEdit), selector.Row(btnChangeTitle), selector.Row(btnExit))
} else {
selector.Inline(
selector.Row(btnAdd), selector.Row(btnDel), selector.Row(btnDelset), selector.Row(btnChangeTitle), selector.Row(btnExit))
}
return c.Send(fmt.Sprintf(`
ID: %s
Title: %s
What do you want to edit? Please select below:
您想要修改貼圖包的甚麼內容? 請選擇:`,
users.data[c.Sender().ID].stickerData.id,
ud.stickerData.id,
ud.stickerData.title),
selector, tele.ModeHTML)
}
func sendAskSDel(c tele.Context) error {
return c.Send("Which sticker do you want to delete? Please send it.\n" +
"您想要刪除哪一個貼圖? 請傳送那個貼圖")
}
func sendConfirmDelset(c tele.Context) error {
selector := &tele.ReplyMarkup{}
btnYes := selector.Data("Yes", CB_YES)
btnNo := selector.Data("No", CB_NO)
selector.Inline(selector.Row(btnYes), selector.Row(btnNo))
return c.Send("You are attempting to delete the whole sticker set, please confirm.\n"+
"您將要刪除整個貼圖包, 請確認.", selector)
}
func sendSFromSS(c tele.Context, ssid string, reply *tele.Message) error {
ss, _ := c.Bot().StickerSet(ssid)
if reply != nil {
c.Bot().Reply(reply, &ss.Stickers[0])
} else {
c.Send(&ss.Stickers[0])
}
return nil
}
func sendFLWarning(c tele.Context) error {
return c.Send(`
It might take longer to process this sticker set (2-8 minutes)...
This warning indicates that you might triggered Telegram's flood limit, and bot is trying to re-submit.
Due to this mechanism, resulted sticker set might contains duplicate or missing sticker, please check manually after done.
此貼圖包可能需要更長時間處理(2-8分鐘)...
看到這一條警告表示Telegram可能限制了您創建貼圖包的頻度, 且bot正在自動嘗試重新製作, 因此得出的貼圖包可能會重複或缺失貼圖, 請在完成製作後再檢查一下.
`)
}
func sendTooManyFloodLimits(c tele.Context) error {
return 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" +
"抱歉, 您似乎觸發了Telegram的貼圖製作次數限制, 建議您過一段時間後再試一次.")
}
func sendNoCbWarn(c tele.Context) error {
return c.Send("Please press a button! /quit\n請選擇按鈕!")
}
func sendBadIDWarn(c tele.Context) error {
return c.Send(`
Bad ID. try again or press Auto Generate. /quit
Can contain alphanum and underscore only, must begin with alphabet, must not contain consecutive underscores.
只可以含有英文,數字,下劃線, 必須由英文字母開頭,不可以有連續下劃線.
ID錯誤, 請試多一次或按下'自動生成'按鈕. /quit`)
}
func sendIDOccupiedWarn(c tele.Context) error {
return c.Send("ID already occupied! try another one. ID已經被占用, 請試試另一個.")
}
func sendBadImportLinkWarn(c tele.Context) error {
return c.Send("Invalid import link, make sure its a LINE Store link or kakao store link. Try again or /quit\n"+
"無效的連結, 請檢視是否為LINE貼圖商店的連結, 或是kakao emoticon的連結.\n\n"+
"For example: 例如:\n"+
"https://store.line.me/stickershop/product/7673/ja\n"+
"https://e.kakao.com/t/pretty-all-friends", tele.ModeHTML)
}
func sendNoSToManage(c tele.Context) error {
return c.Send("Sorry, you have not created any sticker set yet. You can use /import or /create .\n" +
"抱歉, 您還未創建過貼圖包, 您可以使用 /create 或 /import .")
}
func sendPromptStopAdding(c tele.Context) error {
selector := &tele.ReplyMarkup{}
btnDone := selector.Data("Done adding/停止添加", CB_DONE_ADDING)
selector.Inline(selector.Row(btnDone))
return c.Send("Continue sending files or press button below to stop adding.\n"+
"請繼續傳送檔案. 或者按下方按鈕來停止增添.", selector)
}
func replySFileOK(c tele.Context, count int) error {
selector := &tele.ReplyMarkup{}
btnDone := selector.Data("Done adding/停止添加", CB_DONE_ADDING)
selector.Inline(selector.Row(btnDone))
return c.Reply(
fmt.Sprintf("File OK. Got %d stickers. Continue sending files or press button below to stop adding.\n"+
"檔案OK. 已收到%d份貼圖. 請繼續傳送檔案. 或者按下方按鈕來停止增添.", count, count), selector)
}
func sendSEditOK(c tele.Context) error {
return c.Send(
"Successfully edited sticker set. /start\n" +
"成功修改貼圖包. /start")
}
func sendStickerSetFullWarning(c tele.Context) error {
return c.Send(
"Warning: Your sticker set is already full. You cannot add new sticker.\n" +
"提示:當前貼圖包已滿,您將不能增添貼圖。")
}
// func sendEditingEmoji(c tele.Context) error {
// return c.Send("Commiting changes...\n正在套用變更,請稍候...")
// }
func sendAskSearchKeyword(c tele.Context) error {
return c.Send("Please send a word that you want to search\n請傳送想要搜尋的內容")
}
func sendSearchNoResult(c tele.Context) error {
message := "Sorry, no result.\n抱歉, 搜尋沒有結果."
if c.Chat().Type == tele.ChatPrivate {
message += "\nTry again or /quit\n請試試別的關鍵字或 /quit"
}
return c.Send(message)
}
func sendNotifyNoSessionSearch(c tele.Context) error {
return c.Send("Here are some search results, use /search to dig deeper or /start to see available commands.\n" +
"這些是貼圖包搜尋結果,使用 /search 詳細搜尋或 /start 來看看可用的指令。")
}
func sendUnsupportedCommandForGroup(c tele.Context) error {
return c.Send("This command is not supported in group chat, please chat with bot directly.\n" +
"此指令無法於群組內使用, 請與bot直接私訊.")
}
func sendBadSearchKeyword(c tele.Context) error {
return c.Send(fmt.Sprintf(`
Please specify keyword
請指定搜尋關鍵字.
Example: 例如:
/search@%s keyword1 keyword2 ...
/search@%s nekomimi mia
`, botName, botName))
}
func sendPreferKakaoShareLinkWarning(c tele.Context) error {
msg := `
The link you sent is a Kakao store link.
Use a share link for improved image quality and animated sticker support,
you can obtain it from KakaoTalk app by tapping share->copy link in sticker store.
您傳送的是Kakao商店的連結.
使用分享連結才能支援動態貼圖, 靜態貼圖的畫質也更高。
您可以在KakaoTalk App內的貼圖商店點選 分享->複製連結 來取得分享連結。
eg:例如: https://emoticon.kakao.com/items/lV6K2fWmU7CpXlHcP9-ysQJx9rg=?referer=share_link
`
err := c.Reply(&tele.Photo{
File: tele.File{FileID: FID_KAKAO_SHARE_LINK},
Caption: msg,
}, tele.ModeHTML)
if err != nil {
c.Reply(msg, tele.ModeHTML)
}
return nil
}
func sendUseCommandToImport(c tele.Context) error {
return c.Send("Please use /create to create sticker set using your own photos and videos. /start\n" +
"請使用 /create 指令來使用自己的圖片和影片和創建貼圖包. /start")
}
func sendOneStickerFailedToAdd(c tele.Context, pos int, err error) error {
return c.Reply(fmt.Sprintf(`
Failed to add one sticker.
一張貼圖添加失敗.
Index: %d
Error: %s
`, pos, err.Error()))
}
func sendBadSNWarn(c tele.Context) error {
return c.Reply("Wrong sticker or link!\n貼圖或連結錯誤!")
}
func sendSSTitleChanged(c tele.Context) error {
msg := `
Successfully changed title.
新標題設定完成`
return c.Reply(msg, tele.ModeHTML)
}
func sendSSTitleFailedToChanged(c tele.Context) error {
msg := `
Failed to change title, please try again.
新標題設定失敗,請再試一次。`
return c.Reply(msg, tele.ModeHTML)
}
// func sendInvalidEmojiWarn(c tele.Context) error {
// return c.Reply(`
// Sorry, this emoji is invalid, it has been defaulted to ⭐️, you can edit it after done by using /manage command.
// 抱歉,這個emoji無效,並且已默認設定為⭐️,你可以在完成製作後使用 /manage 來修改。
// `)
// }
func sendProcessingStickers(c tele.Context) error {
return c.Send(`
Processing stickers, please wait a while...
正在製作貼圖,請稍等...
`)
}
================================================
FILE: core/os_util.go
================================================
package core
import (
"os"
"path/filepath"
"time"
log "github.com/sirupsen/logrus"
)
func purgeOutdatedStorageData() {
dirEntries, _ := os.ReadDir(dataDir)
for _, f := range dirEntries {
if !f.IsDir() {
continue
}
fInfo, _ := f.Info()
fMTime := fInfo.ModTime().Unix()
fPath := filepath.Join(dataDir, f.Name())
// 1 Day
if fMTime < (time.Now().Unix() - 86400) {
os.RemoveAll(fPath)
users.mu.Lock()
for uid, ud := range users.data {
if ud.sessionID == f.Name() {
log.Warnf("Found outdated user data. Purging from map as well. SID:%s, UID:%d", ud.sessionID, uid)
delete(users.data, uid)
break
}
}
users.mu.Unlock()
log.Infoln("Purged outdated user dir:", fPath)
}
}
if msbconf.WebappDataDir != "" {
webappDataDirEntries, _ := os.ReadDir(msbconf.WebappDataDir)
for _, f := range webappDataDirEntries {
if !f.IsDir() {
continue
}
fInfo, _ := f.Info()
fMTime := fInfo.ModTime().Unix()
fPath := filepath.Join(msbconf.WebappDataDir, f.Name())
// 2 Days
if fMTime < (time.Now().Unix() - 172800) {
os.RemoveAll(fPath)
log.Infoln("Purged outdated webapp data dir:", fPath)
}
}
}
}
================================================
FILE: core/states.go
================================================
package core
import (
"errors"
"path"
"path/filepath"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
"github.com/star-39/moe-sticker-bot/pkg/msbimport"
tele "gopkg.in/telebot.v3"
)
// Handle conversation state during a command.
func handleMessage(c tele.Context) error {
var err error
command, state := getState(c)
if command == "" {
return handleNoSession(c)
}
switch command {
case "import":
switch state {
case "waitSTitle":
err = waitSTitle(c)
case "waitEmojiChoice":
err = waitEmojiChoice(c)
case "process":
err = stateProcessing(c)
case "waitSEmojiAssign":
err = waitSEmojiAssign(c)
}
case "create":
switch state {
case "waitSType":
err = waitSType(c)
case "waitSTitle":
err = waitSTitle(c)
case "waitSID":
err = waitSID(c)
case "waitSFile":
err = waitSFile(c)
case "waitEmojiChoice":
err = waitEmojiChoice(c)
case "waitSEmojiAssign":
err = waitSEmojiAssign(c)
case "process":
err = stateProcessing(c)
}
case "manage":
switch state {
case "waitCbEditChoice":
err = waitCbEditChoice(c)
case "waitSFile":
err = waitSFile(c)
case "waitEmojiChoice":
err = waitEmojiChoice(c)
case "waitSEmojiAssign":
err = waitSEmojiAssign(c)
case "waitSTitle":
err = waitSTitle(c)
case "waitSDel":
err = waitSDel(c)
case "waitCbDelset":
err = waitCbDelset(c)
case "process":
err = stateProcessing(c)
}
case "search":
switch state {
case "waitSearchKW":
err = waitSearchKeyword(c)
}
case "getfid":
err = cmdGetFID(c)
}
return err
}
// Received bare message without using a command.
func handleNoSession(c tele.Context) error {
log.Debugf("user %d entered no session with message: %s", c.Sender().ID, c.Message().Text)
//During previous stage, bot will reply to a message with callback buttons.
//Now we react to user's choice.
if c.Callback() != nil && c.Message().ReplyTo != nil {
switch c.Callback().Data {
case CB_DN_SINGLE:
return downloadStickersAndSend(c.Message().ReplyTo.Sticker, "", c)
case CB_DN_WHOLE:
id := getSIDFromMessage(c.Message().ReplyTo)
return downloadStickersAndSend(nil, id, c)
case CB_MANAGE:
return statePrepareSManage(c)
case CB_OK_IMPORT:
return confirmImport(c, false)
case CB_OK_IMPORT_EMOJI:
return confirmImport(c, true)
case CB_OK_DN:
ud := initUserData(c, "download", "process")
c.Send("Please wait...")
msbimport.ParseImportLink(findLink(c.Message().ReplyTo.Text), ud.lineData)
return downloadLineSToZip(c, ud)
case CB_EXPORT_WA:
hex := secHex(6)
id := getSIDFromMessage(c.Message().ReplyTo)
ss, _ := c.Bot().StickerSet(id)
go prepareWebAppExportStickers(ss, hex)
return sendConfirmExportToWA(c, id, hex)
case CB_BYE:
return c.Send("Bye. /start")
}
}
// bare sticker, ask user's choice.
if c.Message().Sticker != nil {
if matchUserS(c.Sender().ID, c.Message().Sticker.SetName) {
return sendAskSChoice(c, c.Message().Sticker.SetName)
} else {
return sendAskSDownloadChoice(c, c.Message().Sticker)
}
}
//Animation is MP4 video with no sound.
if c.Message().Animation != nil {
return downloadGifToZip(c)
}
if c.Message().Photo != nil || c.Message().Document != nil {
return sendUseCommandToImport(c)
}
// bare text message, expect a link, if no link, search keyword.
link, tp := findLinkWithType(c.Message().Text)
switch tp {
case LINK_TG:
if matchUserS(c.Sender().ID, path.Base(link)) {
return sendAskTGLinkChoice(c)
} else {
return sendAskWantSDown(c)
}
case LINK_IMPORT:
ld := &msbimport.LineData{}
warn, err := msbimport.ParseImportLink(link, ld)
if err != nil {
return sendBadImportLinkWarn(c)
}
if warn != "" {
switch warn {
case msbimport.WARN_KAKAO_PREFER_SHARE_LINK:
sendPreferKakaoShareLinkWarning(c)
}
}
sendNotifySExist(c, ld.Id)
return sendAskWantImportOrDownload(c, ld.IsEmoji)
default:
if c.Message().Text == "" {
return sendNoSessionWarning(c)
}
// User sent plain text, attempt to search.
if trySearchKeyword(c) {
return sendNotifyNoSessionSearch(c)
} else {
return sendNoSessionWarning(c)
}
}
}
func confirmImport(c tele.Context, wantEmoji bool) error {
ud := initUserData(c, "import", "waitSTitle")
_, err := msbimport.ParseImportLink(findLink(c.Message().ReplyTo.Text), ud.lineData)
if err != nil {
return err
}
ud.stickerData.id = checkGnerateSIDFromLID(ud.lineData)
workDir := filepath.Join(ud.workDir, ud.lineData.Id)
sendAskTitle_Import(c)
ud.wg.Add(1)
err = msbimport.PrepareImportStickers(ud.ctx, ud.lineData, workDir, true, wantEmoji)
ud.wg.Done()
if err != nil {
return err
}
ud.stickerData.lAmount = ud.lineData.Amount
ud.stickerData.isVideo = ud.lineData.IsAnimated
if ud.lineData.IsEmoji && wantEmoji {
ud.stickerData.stickerSetType = tele.StickerCustomEmoji
ud.stickerData.isCustomEmoji = true
} else {
ud.stickerData.stickerSetType = tele.StickerRegular
}
//After PrepareImportStickers returns, individual LineFile might not be ready yet.
//When transfering data to ud.stickerData.stickers, make sure to transfer finished data only.
for range ud.lineData.Files {
sf := &StickerFile{}
sf.wg.Add(1)
ud.stickerData.stickers = append(ud.stickerData.stickers, sf)
}
for i, lf := range ud.lineData.Files {
lf.Wg.Wait()
ud.stickerData.stickers[i].wg.Done()
ud.stickerData.stickers[i].oPath = lf.OriginalFile
ud.stickerData.stickers[i].cPath = lf.ConvertedFile
}
return nil
}
func trySearchKeyword(c tele.Context) bool {
keywords := strings.Split(c.Text(), " ")
if len(keywords) == 0 {
return false
}
lines := searchLineS(keywords)
if len(lines) == 0 {
return false
}
sendSearchResult(20, lines, c)
return true
}
func stateProcessing(c tele.Context) error {
if c.Callback() != nil {
if c.Callback().Data == "bye" {
return cmdQuit(c)
}
}
return c.Send("Processing, please wait... 作業中, 請稍後... /quit")
}
func statePrepareSManage(c tele.Context) error {
var ud *UserData
if c.Message().ReplyTo == nil {
return errors.New("unknown error: no reply to")
}
ud = initUserData(c, "manage", "waitCbEditChoice")
id := getSIDFromMessage(c.Message().ReplyTo)
ud.stickerData.id = id
ud.lastContext = c
// Allow admin to manage all sticker sets.
if c.Sender().ID == msbconf.AdminUid {
goto NEXT
}
if !matchUserS(c.Sender().ID, ud.stickerData.id) {
return c.Send("Sorry, this sticker set cannot be edited. try another or /quit")
}
NEXT:
err := retrieveSSDetails(c, ud.stickerData.id, ud.stickerData)
if err != nil {
return c.Send("bad sticker set! try again or /quit")
}
err = prepareWebAppEditStickers(users.data[c.Sender().ID])
if err != nil {
return c.Send("error preparing stickers for webapp /quit")
}
if ud.stickerData.cAmount == 120 {
sendStickerSetFullWarning(c)
}
setState(c, "waitCbEditChoice")
return sendAskEditChoice(c)
}
func waitCbEditChoice(c tele.Context) error {
if c.Callback() == nil {
return sendNoCbWarn(c)
}
switch c.Callback().Data {
case CB_ADD_STICKER:
setState(c, "waitSFile")
return sendAskStickerFile(c)
case CB_DELETE_STICKER:
setState(c, "waitSDel")
return sendAskSDel(c)
case CB_DELETE_STICKER_SET:
setState(c, "waitCbDelset")
return sendConfirmDelset(c)
case CB_CHANGE_TITLE:
setState(c, "waitSTitle")
return sendAskTitle(c)
case CB_BYE:
endManageSession(c)
terminateSession(c)
default:
return sendInStateWarning(c)
}
return nil
}
func waitSDel(c tele.Context) error {
ud := users.data[c.Sender().ID]
if c.Message().Sticker == nil {
return c.Send("send sticker! try again or /quit")
}
if c.Message().Sticker.SetName != ud.stickerData.id {
return c.Send("wrong sticker! try again or /quit")
}
err := c.Bot().DeleteSticker(c.Message().Sticker.FileID)
if err != nil {
c.Send("error deleting sticker! try another one or /quit")
return err
}
c.Send("Delete OK. 成功刪除一張貼圖。")
ud.stickerData.cAmount--
if ud.stickerData.cAmount == 0 {
deleteUserS(ud.stickerData.id)
deleteLineS(ud.stickerData.id)
terminateSession(c)
return nil
} else {
setState(c, "waitCbEditChoice")
return sendAskEditChoice(c)
}
}
func waitCbDelset(c tele.Context) error {
if c.Callback() == nil {
setState(c, "waitCbEditChoice")
return sendAskEditChoice(c)
}
if c.Callback().Data != CB_YES {
setState(c, "waitCbEditChoice")
return sendAskEditChoice(c)
}
ud := users.data[c.Sender().ID]
setState(c, "process")
c.Send("please wait...")
ss, _ := c.Bot().StickerSet(ud.stickerData.id)
for _, s := range ss.Stickers {
c.Bot().DeleteSticker(s.FileID)
}
deleteUserS(ud.stickerData.id)
deleteLineS(ud.stickerData.id)
c.Send("Delete set OK. bye")
endManageSession(c)
terminateSession(c)
return nil
}
func waitSType(c tele.Context) error {
if c.Callback() == nil {
return c.Send("Please press a button. /quit")
}
ud := users.data[c.Sender().ID]
if strings.Contains(c.Callback().Data, CB_CUSTOM_EMOJI) {
ud.stickerData.stickerSetType = tele.StickerCustomEmoji
ud.stickerData.isCustomEmoji = true
} else {
ud.stickerData.stickerSetType = tele.StickerRegular
ud.stickerData.isCustomEmoji = false
}
sendAskTitle(c)
setState(c, "waitSTitle")
return nil
}
func waitSFile(c tele.Context) error {
if c.Callback() != nil {
switch c.Callback().Data {
case CB_DONE_ADDING:
goto NEXT
case CB_BYE:
terminateSession(c)
return nil
default:
return sendPromptStopAdding(c)
}
}
if c.Message().Media() != nil {
err := appendMedia(c)
if err != nil {
c.Reply("Failed processing this file. 處理此檔案時錯誤:\n" + err.Error())
}
return nil
} else {
return sendPromptStopAdding(c)
}
NEXT:
if len(users.data[c.Sender().ID].stickerData.stickers) == 0 {
return c.Send("No image received. try again or /quit")
}
setState(c, "waitEmojiChoice")
sendAskEmoji(c)
return nil
}
func waitSTitle(c tele.Context) error {
ud := users.data[c.Sender().ID]
command := ud.command
// User sent text instead of clicking button.
if c.Callback() == nil {
if command == "create" || command == "import" {
ud.stickerData.title = c.Message().Text
} else if command == "manage" {
err := c.Bot().SetStickerSetTitle(c.Recipient(), c.Message().Text, ud.stickerData.id)
setState(c, "waitCbEditChoice")
if err != nil {
log.Warnln(err)
return sendSSTitleFailedToChanged(c)
} else {
return sendSSTitleChanged(c)
}
} else {
return nil
}
// User clicked a button, only command "import" is allowed.
} else {
//Reject.
if command != "import" {
return nil
}
titleIndex, atoiErr := strconv.Atoi(c.Callback().Data)
if atoiErr == nil && titleIndex != -1 {
ud.stickerData.title = ud.lineData.I18nTitles[titleIndex] + " @" + botName
} else {
ud.stickerData.title = ud.lineData.Title + " @" + botName
}
}
if !checkTitle(ud.stickerData.title) {
return c.Send("bad title! try again or /quit")
}
switch command {
case "import":
setState(c, "waitEmojiChoice")
return sendAskEmoji(c)
case "create":
setState(c, "waitSID")
sendAskID(c)
}
return nil
}
func waitSID(c tele.Context) error {
var id string
if c.Callback() != nil {
if c.Callback().Data == "auto" {
users.data[c.Sender().ID].stickerData.id = "sticker_" + secHex(4) + "_by_" + botName
goto NEXT
}
}
id = regexAlphanum.FindString(c.Message().Text)
if !checkID(id) {
return sendBadIDWarn(c)
}
id = id + "_by_" + botName
if _, err := c.Bot().StickerSet(id); err == nil {
return sendIDOccupiedWarn(c)
}
users.data[c.Sender().ID].stickerData.id = id
NEXT:
setState(c, "waitSFile")
return sendAskStickerFile(c)
}
func waitEmojiChoice(c tele.Context) error {
ud := users.data[c.Sender().ID]
if c.Callback() != nil {
switch c.Callback().Data {
case "random":
users.data[c.Sender().ID].stickerData.emojis = []string{"⭐"}
case "manual":
sendProcessStarted(ud, c, "preparing...")
setState(c, ST_PROCESSING)
ud.wg.Wait()
for range ud.stickerData.stickers {
ud.commitChans = append(ud.commitChans, make(chan bool))
}
setState(c, "waitSEmojiAssign")
return sendAskEmojiAssign(c)
default:
return nil
}
} else {
emojis := findEmojis(c.Message().Text)
if emojis == "" {
return c.Reply("Send emoji or press button a button.\n請傳送emoji或點選按鈕。 /quit")
}
users.data[c.Sender().ID].stickerData.emojis = []string{emojis}
}
setState(c, ST_PROCESSING)
err := submitStickerSetAuto(!(ud.command == "manage"), c)
endSession(c)
if err != nil {
return err
}
return nil
}
func waitSEmojiAssign(c tele.Context) error {
emojiList := findEmojiList(c.Message().Text)
if len(emojiList) == 0 {
return c.Reply("Please send emoji and keywords(optional).\n請傳送emoji和 關鍵字(可選)。\ntry again or /quit")
}
keywords := stripEmoji(c.Message().Text)
keywordList := []string{}
if len(keywords) > 0 {
keywordList = strings.Split(keywords, " ")
}
ud := users.data[c.Sender().ID]
setState(c, ST_PROCESSING)
err := submitStickerManual(!(users.data[c.Sender().ID].command == "manage"), ud.stickerData.pos, emojiList, keywordList, c)
if err != nil {
return err
}
ud.stickerData.pos += 1
if ud.stickerData.pos == ud.stickerData.lAmount {
return sendProcessingStickers(c)
} else {
sendAskEmojiAssign(c)
setState(c, "waitSEmojiAssign")
return nil
}
}
func waitSearchKeyword(c tele.Context) error {
keywords := strings.Split(c.Text(), " ")
lines := searchLineS(keywords)
if len(lines) == 0 {
return sendSearchNoResult(c)
}
sendSearchResult(-1, lines, c)
terminateSession(c)
return nil
}
================================================
FILE: core/sticker.go
================================================
package core
import (
"errors"
"math/rand"
"os"
"path/filepath"
"strconv"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/star-39/moe-sticker-bot/pkg/msbimport"
tele "gopkg.in/telebot.v3"
)
//TODO: Shrink oversized function.
// Final stage of automated sticker submission.
// Automated means all emojis are same.
func submitStickerSetAuto(createSet bool, c tele.Context) error {
uid := c.Sender().ID
ud := users.data[uid]
pText, teleMsg, _ := sendProcessStarted(ud, c, "Waiting...")
ud.wg.Wait()
defer cleanUserData(uid)
if len(ud.stickerData.stickers) == 0 {
log.Error("No sticker to commit!")
return errors.New("no sticker available")
}
log.Debugln("stickerData summary:")
log.Debugln(ud.stickerData)
committedStickers := 0
errorCount := 0
flCount := &ud.stickerData.flCount
ssName := ud.stickerData.id
ssTitle := ud.stickerData.title
ssType := ud.stickerData.stickerSetType
//Set emojis and keywords in batch.
for _, s := range ud.stickerData.stickers {
s.emojis = ud.stickerData.emojis
s.keywords = MSB_DEFAULT_STICKER_KEYWORDS
}
//Try batch create.
var batchCreateSuccess bool
if createSet {
err := createStickerSetBatch(ud.stickerData.stickers, c, ssName, ssTitle, ssType)
if err != nil {
log.Warnln("sticker.go: Error batch create:", err.Error())
} else {
log.Debugln("sticker.go: Batch create success.")
batchCreateSuccess = true
if len(ud.stickerData.stickers) < 51 {
committedStickers = len(ud.stickerData.stickers)
} else {
committedStickers = 50
}
}
}
//One by one commit.
for index, sf := range ud.stickerData.stickers {
var err error
//Sticker set already finished.
if batchCreateSuccess && len(ud.stickerData.stickers) < 51 {
go editProgressMsg(len(ud.stickerData.stickers), len(ud.stickerData.stickers), "", pText, teleMsg, c)
break
}
//Sticker set is larger than 50 and batch succeeded.
//Skip first 50 stickers.
if batchCreateSuccess && len(ud.stickerData.stickers) > 50 {
if index < 50 {
continue
}
}
//Batch creation failed, run normal creation procedure if createSet is true.
if createSet && index == 0 {
err = createStickerSet(false, sf, c, ssName, ssTitle, ssType)
if err != nil {
log.Errorln("create sticker set failed!. ", err)
return err
} else {
committedStickers += 1
}
continue
}
go editProgressMsg(index, len(ud.stickerData.stickers), "", pText, teleMsg, c)
err = commitSingleticker(index, flCount, false, sf, c, ssName, ssType)
if err != nil {
log.Warnln("execAutoCommit: a sticker failed to add.", err)
sendOneStickerFailedToAdd(c, index, err)
errorCount += 1
} else {
log.Debugln("one sticker commited. count: ", committedStickers)
committedStickers += 1
}
// If encountered flood limit more than once, set a interval.
if *flCount == 1 {
sleepTime := 10 + rand.Intn(10)
time.Sleep(time.Duration(sleepTime) * time.Second)
} else if *flCount > 1 {
sleepTime := 60 + rand.Intn(10)
time.Sleep(time.Duration(sleepTime) * time.Second)
}
}
// Tolerate at most 3 errors when importing sticker set.
if ud.command == "import" && errorCount > 3 {
return errors.New("too many errors importing")
}
if createSet {
if ud.command == "import" {
insertLineS(ud.lineData.Id, ud.lineData.Link, ud.stickerData.id, ud.stickerData.title, true)
// Only verify for import.
// User generated sticker set might intentionally contain same stickers.
if *flCount > 1 {
verifyFloodedStickerSet(c, *flCount, errorCount, ud.lineData.Amount, ud.stickerData.id)
}
}
insertUserS(c.Sender().ID, ud.stickerData.id, ud.stickerData.title, time.Now().Unix())
}
editProgressMsg(0, 0, "Success! /start", pText, teleMsg, c)
sendSFromSS(c, ud.stickerData.id, teleMsg)
return nil
}
// Only fatal error should be returned.
func submitStickerManual(createSet bool, pos int, emojis []string, keywords []string, c tele.Context) error {
ud := users.data[c.Sender().ID]
var err error
name := ud.stickerData.id
title := ud.stickerData.title
ssType := ud.stickerData.stickerSetType
if len(ud.stickerData.stickers) == 0 {
log.Error("No sticker to commit!!")
return errors.New("no sticker available")
}
sf := ud.stickerData.stickers[pos]
sf.emojis = emojis
sf.keywords = keywords
//Do not submit to goroutine when creating sticker set.
if createSet && pos == 0 {
defer close(ud.commitChans[pos])
err = createStickerSet(false, sf, c, name, title, ssType)
if err != nil {
log.Errorln("create failed. ", err)
return err
} else {
ud.stickerData.cAmount += 1
}
if ud.stickerData.lAmount == 1 {
return finalizeSubmitStickerManual(c, createSet, ud)
}
} else {
go func() {
//wait for the previous commit to be done.
if pos > 0 {
<-ud.commitChans[pos-1]
}
err = commitSingleticker(pos, &ud.stickerData.flCount, false, sf, c, name, ssType)
if err != nil {
sendOneStickerFailedToAdd(c, pos, err)
log.Warnln("execEmojiAssign: a sticker failed to add: ", err)
} else {
ud.stickerData.cAmount += 1
}
if pos+1 == ud.stickerData.lAmount {
finalizeSubmitStickerManual(c, createSet, ud)
}
close(ud.commitChans[pos])
}()
}
return nil
}
func finalizeSubmitStickerManual(c tele.Context, createSet bool, ud *UserData) error {
if createSet {
if ud.command == "import" {
insertLineS(ud.lineData.Id, ud.lineData.Link, ud.stickerData.id, ud.stickerData.title, false)
}
insertUserS(c.Sender().ID, ud.stickerData.id, ud.stickerData.title, time.Now().Unix())
}
sendExecEmojiAssignFinished(c)
// c.Send("Success! /start")
sendSFromSS(c, ud.stickerData.id, nil)
endSession(c)
return nil
}
// Create sticker set if needed.
func createStickerSet(safeMode bool, sf *StickerFile, c tele.Context, name string, title string, ssType string) error {
var file string
var isCustomEmoji bool
if ssType == tele.StickerCustomEmoji {
isCustomEmoji = true
}
sf.wg.Wait()
if safeMode {
file, _ = msbimport.FFToWebmSafe(sf.oPath, isCustomEmoji)
} else {
file = sf.cPath
}
log.Debugln("createStickerSet: attempting, sticker file path:", sf.cPath)
input := tele.InputSticker{
Emojis: sf.emojis,
Keywords: sf.keywords,
}
if sf.fileID != "" {
input.Sticker = sf.fileID
input.Format = sf.format
} else {
input.Sticker = "file://" + file
input.Format = guessInputStickerFormat(file)
}
err := c.Bot().CreateStickerSet(c.Recipient(), []tele.InputSticker{input}, name, title, ssType)
if err == nil {
return nil
}
log.Errorf("createStickerSet error:%s for set:%s.", err, name)
// Only handle video_long error here, return all other error types.
if strings.Contains(strings.ToLower(err.Error()), "video_long") {
// Redo with safe mode on.
// This should happen only one time.
// So if safe mode is on and this error still occurs, return err.
if safeMode {
log.Error("safe mode DID NOT resolve video_long problem.")
return err
} else {
log.Warnln("returned video_long, attempting safe mode.")
return createStickerSet(true, sf, c, name, title, ssType)
}
} else {
return err
}
}
// Create sticker set with multiple StickerFile.
// API 7.2 feature, consider it experimental.
// If it failed, no retry, just return error and we try conventional way.
func createStickerSetBatch(sfs []*StickerFile, c tele.Context, name string, title string, ssType string) error {
var inputs []tele.InputSticker
log.Debugln("createStickerSetBatch: attempting, batch creation:", name)
for i, sf := range sfs {
sf.wg.Wait()
file := sf.cPath
input := tele.InputSticker{
Emojis: sf.emojis,
Keywords: sf.keywords,
}
if sf.fileID != "" {
input.Sticker = sf.fileID
input.Format = sf.format
} else {
input.Sticker = "file://" + file
input.Format = guessInputStickerFormat(file)
}
inputs = append(inputs, input)
//Up to 50 stickers.
if i == 49 {
break
}
}
return c.Bot().CreateStickerSet(c.Recipient(), inputs, name, title, ssType)
}
// Commit single sticker, retry happens inside this function.
// If all retries failed, return err.
//
// flCount counts the total flood limit for entire sticker set.
// pos is for logging only.
func commitSingleticker(pos int, flCount *int, safeMode bool, sf *StickerFile, c tele.Context, name string, ssType string) error {
var err error
var floodErr tele.FloodError
var file string
var isCustomEmoji bool
if ssType == tele.StickerCustomEmoji {
isCustomEmoji = true
}
sf.wg.Wait()
if safeMode {
file, _ = msbimport.FFToWebmSafe(sf.oPath, isCustomEmoji)
} else {
file = sf.cPath
}
log.Debugln("commitSingleticker: attempting, sticker file path:", sf.cPath)
// Retry loop.
// For each sticker, retry at most 2 times, means 3 commit attempts in total.
for i := 0; i < 3; i++ {
input := tele.InputSticker{
Emojis: sf.emojis,
Keywords: sf.keywords,
}
if sf.fileID != "" {
input.Sticker = sf.fileID
input.Format = sf.format
} else {
input.Sticker = "file://" + file
input.Format = guessInputStickerFormat(file)
}
err = c.Bot().AddSticker(c.Recipient(), input, name)
if err == nil {
return nil
}
log.Errorf("commit sticker error:%s for set:%s.", err, name)
// This flood limit error only happens to a specific user at a specific time.
// It is "fake" most of time, since TDLib in API Server will automatically retry.
// However, API always return 429.
// Since API side will always do retry at TDLib level, message_id was also being kept so
// no position shift will happen.
// Flood limit error could be probably ignored.
if errors.As(err, &floodErr) {
// This reflects the retry count for entire SS.
*flCount += 1
log.Warnf("commitSticker: Flood limit encountered for user:%d, set:%s, count:%d, pos:%d", c.Sender().ID, name, *flCount, pos)
log.Warnln("commitSticker: commit sticker retry after: ", floodErr.RetryAfter)
if *flCount == 2 {
sendFLWarning(c)
}
//Sleep
if floodErr.RetryAfter > 60 {
log.Error("RA too long! Telegram's bug? Attempt to sleep for 120 seconds.")
time.Sleep(120 * time.Second)
} else {
extraRA := *flCount * 15
log.Warnf("Sleeping for %d seconds due to FL.", floodErr.RetryAfter+extraRA)
time.Sleep(time.Duration(floodErr.RetryAfter+extraRA) * time.Second)
}
log.Warnf("Woken up from RA sleep. ignoring this error. user:%d, set:%s, count:%d, pos:%d", c.Sender().ID, name, *flCount, pos)
//According to collected logs, exceeding 2 flood counts will sometimes cause api server to stop auto retrying.
//Hence, we do retry here, else, break retry loop.
if *flCount > 2 {
continue
} else {
break
}
} else if strings.Contains(strings.ToLower(err.Error()), "video_long") {
// Redo with safe mode on.
// This should happen only one time.
// So if safe mode is on and this error still occurs, return err.
if safeMode {
log.Error("safe mode DID NOT resolve video_long problem.")
return err
} else {
log.Warnln("returned video_long, attempting safe mode.")
return commitSingleticker(pos, flCount, true, sf, c, name, ssType)
}
} else if strings.Contains(err.Error(), "400") {
// return remaining 400 BAD REQUEST immediately to parent without retry.
return err
} else if strings.Contains(err.Error(), "invalid sticker emojis") {
log.Warn("commitSticker: invalid emoji, resetting to a star emoji and retrying...")
input.Emojis = []string{"⭐️"}
} else {
// Handle unknown error here.
// We simply retry for 2 more times with 5 sec interval.
log.Warnln("commitSticker: retrying... cause:", err)
time.Sleep(5 * time.Second)
}
}
log.Warn("commitSticker: too many retries")
if errors.As(err, &floodErr) {
log.Warn("commitSticker: reached max retry for flood limit, assume success.")
return nil
}
return err
}
func editStickerEmoji(newEmojis []string, fid string, ud *UserData) error {
return b.SetStickerEmojiList(ud.lastContext.Recipient(), fid, newEmojis)
}
// Receive and process user uploaded media file and convert to Telegram compliant format.
// Accept telebot Media and Sticker only.
func appendMedia(c tele.Context) error {
log.Debugf("appendMedia: Received file, MType:%s, FileID:%s", c.Message().Media().MediaType(), c.Message().Media().MediaFile().FileID)
var files []string
var sfs []*StickerFile
var err error
var workDir string
var savePath string
ud := users.data[c.Sender().ID]
ud.wg.Add(1)
defer ud.wg.Done()
if ud.stickerData.cAmount+len(ud.stickerData.stickers) > 120 {
return errors.New("sticker set already full 此貼圖包已滿")
}
//Incoming media is a sticker.
if c.Message().Sticker != nil && ((c.Message().Sticker.Type == tele.StickerCustomEmoji) == ud.stickerData.isCustomEmoji) {
var format string
if c.Message().Sticker.Video {
format = "video"
} else {
format = "static"
}
sfs = append(sfs, &StickerFile{
fileID: c.Message().Sticker.FileID,
format: format,
})
log.Debugf("One received sticker file OK. ID:%s", c.Message().Sticker.FileID)
goto CONTINUE
}
workDir = users.data[c.Sender().ID].workDir
savePath = filepath.Join(workDir, secHex(4))
if c.Message().Media().MediaType() == "document" {
savePath += filepath.Ext(c.Message().Document.FileName)
} else if c.Message().Media().MediaType() == "animation" {
savePath += filepath.Ext(c.Message().Animation.FileName)
}
err = c.Bot().Download(c.Message().Media().MediaFile(), savePath)
if err != nil {
return errors.New("error downloading media")
}
if guessIsArchive(savePath) {
files = append(files, msbimport.ArchiveExtract(savePath)...)
} else {
files = append(files, savePath)
}
log.Debugln("appendMedia: Media downloaded to savepath:", savePath)
for _, f := range files {
var cf string
var err error
//If incoming media is already a sticker, use the file as is.
if c.Message().Sticker != nil && ((c.Message().Sticker.Type == "custom_emoji") == ud.stickerData.isCustomEmoji) {
cf = f
} else {
cf, err = msbimport.ConverMediaToTGStickerSmart(f, ud.stickerData.isCustomEmoji)
}
if err != nil {
log.Warnln("Failed converting one user sticker", err)
c.Send("Failed converting one user sticker:" + err.Error())
continue
}
sfs = append(sfs, &StickerFile{
oPath: f,
cPath: cf,
})
log.Debugf("One received file OK. oPath:%s | cPath:%s", f, cf)
}
CONTINUE:
if len(sfs) == 0 {
return errors.New("download or convert error")
}
ud.stickerData.stickers = append(ud.stickerData.stickers, sfs...)
ud.stickerData.lAmount = len(ud.stickerData.stickers)
replySFileOK(c, len(ud.stickerData.stickers))
return nil
}
func guessIsArchive(f string) bool {
f = strings.ToLower(f)
archiveExts := []string{".rar", ".7z", ".zip", ".tar", ".gz", ".bz2", ".zst", ".rar5"}
for _, ext := range archiveExts {
if strings.HasSuffix(f, ext) {
return true
}
}
return false
}
func verifyFloodedStickerSet(c tele.Context, fc int, ec int, desiredAmount int, ssn string) {
time.Sleep(31 * time.Second)
ss, err := b.StickerSet(ssn)
if err != nil {
return
}
if desiredAmount < len(ss.Stickers) {
log.Warnf("A flooded sticker set duplicated! floodCount:%d, errorCount:%d, ssn:%s, desired:%d, got:%d", fc, ec, ssn, desiredAmount, len(ss.Stickers))
log.Warnf("Attempting dedup!")
workdir := filepath.Join(dataDir, secHex(8))
os.MkdirAll(workdir, 0755)
for si, s := range ss.Stickers {
if si > 0 {
fp := filepath.Join(workdir, strconv.Itoa(si-1)+".webp")
f := filepath.Join(workdir, strconv.Itoa(si)+".webp")
c.Bot().Download(&s.File, f)
if compCRC32(f, fp) {
b.DeleteSticker(s.FileID)
}
}
}
os.RemoveAll(workdir)
} else if desiredAmount > len(ss.Stickers) {
log.Warnf("A flooded sticker set missing sticker! floodCount:%d, errorCount:%d, ssn:%s, desired:%d, got:%d", fc, ec, ssn, desiredAmount, len(ss.Stickers))
c.Reply("Sorry, this sticker set seems corrupted, please check.\n抱歉, 這個貼圖包似乎有缺失貼圖, 請檢查一下.")
} else {
log.Infof("A flooded sticker set seems ok. floodCount:%d, errorCount:%d, ssn:%s, desired:%d, got:%d", fc, ec, ssn, desiredAmount, len(ss.Stickers))
}
}
================================================
FILE: core/sticker_download.go
================================================
package core
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/panjf2000/ants/v2"
"github.com/star-39/moe-sticker-bot/pkg/msbimport"
tele "gopkg.in/telebot.v3"
)
// When s is not nil, download single sticker,
// otherwise, download whole set from setID.
func downloadStickersAndSend(s *tele.Sticker, setID string, c tele.Context) error {
var id string
if s != nil {
id = s.SetName
} else {
id = setID
}
sID := secHex(8)
ud := &UserData{
workDir: filepath.Join(dataDir, sID),
stickerData: &StickerData{},
lineData: &msbimport.LineData{},
}
workDir := filepath.Join(ud.workDir, id)
os.MkdirAll(workDir, 0755)
var flist []string
var cflist []string
var err error
//Single sticker
if s != nil {
obj := &StickerDownloadObject{
wg: sync.WaitGroup{},
sticker: *s,
dest: filepath.Join(workDir, s.SetName+"_"+s.Emoji),
needConvert: true,
shrinkGif: false,
forWebApp: false,
}
obj.wg.Add(1)
wDownloadStickerObject(obj)
if obj.err != nil {
return err
}
if s.Video || s.Animated {
zip := filepath.Join(workDir, secHex(4)+".zip")
msbimport.FCompress(zip, []string{obj.cf})
c.Bot().Send(c.Recipient(), &tele.Document{FileName: filepath.Base(zip), File: tele.FromDisk(zip)})
} else {
c.Bot().Send(c.Recipient(), &tele.Document{FileName: filepath.Base(obj.cf), File: tele.FromDisk(obj.cf)})
}
return err
}
//Sticker set
ss, err := c.Bot().StickerSet(setID)
if err != nil {
return sendBadSNWarn(c)
}
ud.stickerData.id = ss.Name
ud.stickerData.title = ss.Title
pText, pMsg, _ := sendProcessStarted(ud, c, "")
cleanUserData(c.Sender().ID)
defer os.RemoveAll(workDir)
var wpDownloadSticker *ants.PoolWithFunc
wpDownloadSticker, _ = ants.NewPoolWithFunc(4, wDownloadStickerObject)
defer wpDownloadSticker.Release()
imageTime := time.Now()
var objs []*StickerDownloadObject
for index, s := range ss.Stickers {
obj := &StickerDownloadObject{
wg: sync.WaitGroup{},
sticker: s,
dest: filepath.Join(workDir, fmt.Sprintf("%s_%d_%s", setID, index+1, s.Emoji)),
needConvert: true,
shrinkGif: false,
forWebApp: false,
}
obj.wg.Add(1)
objs = append(objs, obj)
go wpDownloadSticker.Invoke(obj)
}
for i, obj := range objs {
go editProgressMsg(i, len(ss.Stickers), "", pText, pMsg, c)
obj.wg.Wait()
imageTime = imageTime.Add(time.Duration(i+1) * time.Second)
msbimport.SetImageTime(obj.of, imageTime)
flist = append(flist, obj.of)
cflist = append(cflist, obj.cf)
}
go editProgressMsg(0, 0, "Uploading...", pText, pMsg, c)
originalZipPath := filepath.Join(workDir, setID+"_original.zip")
convertedZipPath := filepath.Join(workDir, setID+"_converted.zip")
var zipPaths []string
zipPaths = append(zipPaths, msbimport.FCompressVol(originalZipPath, flist)...)
zipPaths = append(zipPaths, msbimport.FCompressVol(convertedZipPath, cflist)...)
for _, zipPath := range zipPaths {
_, err := c.Bot().Send(c.Recipient(), &tele.Document{FileName: filepath.Base(zipPath), File: tele.FromDisk(zipPath)})
time.Sleep(1 * time.Second)
if err != nil {
return err
}
}
editProgressMsg(0, 0, "success! /start", pText, pMsg, c)
return nil
}
func downloadGifToZip(c tele.Context) error {
c.Reply("Downloading, please wait...\n正在下載, 請稍等...")
workDir := filepath.Join(dataDir, secHex(4))
os.MkdirAll(workDir, 0755)
defer os.RemoveAll(workDir)
f := filepath.Join(workDir, "animation_MP4.mp4")
err := c.Bot().Download(&c.Message().Animation.File, f)
if err != nil {
return err
}
cf, _ := msbimport.FFToGif(f)
cf2 := strings.ReplaceAll(cf, "animation_MP4.mp4", "animation_GIF.gif")
os.Rename(cf, cf2)
zip := filepath.Join(workDir, secHex(4)+".zip")
msbimport.FCompress(zip, []string{cf2})
_, err = c.Bot().Reply(c.Message(), &tele.Document{FileName: filepath.Base(zip), File: tele.FromDisk(zip)})
return err
}
func downloadLineSToZip(c tele.Context, ud *UserData) error {
workDir := filepath.Join(ud.workDir, ud.lineData.Id)
err := msbimport.PrepareImportStickers(ud.ctx, ud.lineData, workDir, false, false)
if err != nil {
return err
}
for _, f := range ud.lineData.Files {
f.Wg.Wait()
}
// workDir := filepath.Dir(ud.lineData.files[0])
zipName := ud.lineData.Id + ".zip"
zipPath := filepath.Join(workDir, zipName)
var files []string
for _, lf := range ud.lineData.Files {
files = append(files, lf.OriginalFile)
}
msbimport.FCompress(zipPath, files)
_, err = c.Bot().Send(c.Recipient(), &tele.Document{FileName: zipName, File: tele.FromDisk(zipPath)})
endSession(c)
return err
}
================================================
FILE: core/userdata.go
================================================
package core
import (
"context"
"os"
"path/filepath"
"strings"
log "github.com/sirupsen/logrus"
"github.com/star-39/moe-sticker-bot/pkg/msbimport"
tele "gopkg.in/telebot.v3"
)
func cleanUserDataAndDir(uid int64) bool {
log.WithField("uid", uid).Debugln("Purging userdata...")
_, exist := users.data[uid]
if exist {
os.RemoveAll(users.data[uid].workDir)
users.mu.Lock()
delete(users.data, uid)
users.mu.Unlock()
log.WithField("uid", uid).Debugln("Userdata purged from map and disk.")
return true
} else {
log.WithField("uid", uid).Debugln("Userdata does not exisst, do nothing.")
return false
}
}
func cleanUserData(uid int64) bool {
log.WithField("uid", uid).Debugln("Purging userdata...")
_, exist := users.data[uid]
if exist {
users.mu.Lock()
delete(users.data, uid)
users.mu.Unlock()
log.WithField("uid", uid).Debugln("Userdata purged from map.")
return true
} else {
log.WithField("uid", uid).Debugln("Userdata does not exist, do nothing.")
return false
}
}
func initUserData(c tele.Context, command string, state string) *UserData {
uid := c.Sender().ID
users.mu.Lock()
ctx, cancel := context.WithCancel(context.Background())
sID := secHex(6)
users.data[uid] = &UserData{
state: state,
sessionID: sID,
// userDir: filepath.Join(dataDir, strconv.FormatInt(uid, 10)),
workDir: filepath.Join(dataDir, sID),
command: command,
lineData: &msbimport.LineData{},
stickerData: &StickerData{},
// stickerManage: &StickerManage{},
ctx: ctx,
cancel: cancel,
}
users.mu.Unlock()
// Do not anitize user work directory.
// os.RemoveAll(users.data[uid].userDir)
os.MkdirAll(users.data[uid].workDir, 0755)
return users.data[uid]
}
func getState(c tele.Context) (string, string) {
ud, exist := users.data[c.Sender().ID]
if exist {
return ud.command, ud.state
} else {
return "", ""
}
}
func checkState(next tele.HandlerFunc) tele.HandlerFunc {
return func(c tele.Context) error {
//If bot is summoned from group chat, check command.
if c.Chat().Type == tele.ChatGroup || c.Chat().Type == tele.ChatSuperGroup {
log.Debugf("User %d attempted command from group chat.", c.Sender().ID)
//For group chat, support /search only.
if strings.HasPrefix(c.Text(), "/search@"+botName) {
return next(c)
} else if strings.Contains(c.Text(), "@"+botName) {
//has metion
return sendUnsupportedCommandForGroup(c)
} else {
//do nothing
return nil
}
}
command, _ := getState(c)
if command == "" {
log.Debugf("User %d entering command with message: %s", c.Sender().ID, c.Message().Text)
return next(c)
} else {
log.Debugf("User %d already in command: %v", c.Sender().ID, command)
return sendInStateWarning(c)
}
}
}
func setState(c tele.Context, state string) {
if c == nil {
return
}
ud, ok := users.data[c.Sender().ID]
if !ok {
return
}
ud.state = state
}
// func setCommand(c tele.Context, command string) {
// uid := c.Sender().ID
// users.data[uid].command = command
// }
================================================
FILE: core/util.go
================================================
package core
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"hash/crc32"
"net/url"
"os"
"os/exec"
"path"
"regexp"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
"github.com/star-39/moe-sticker-bot/pkg/msbimport"
tele "gopkg.in/telebot.v3"
"mvdan.cc/xurls/v2"
)
var regexAlphanum = regexp.MustCompile(`[a-zA-Z0-9_]+`)
// var httpClient = &http.Client{
// Timeout: 5 * time.Second,
// }
func checkTitle(t string) bool {
if len(t) > 128 || len(t) < 1 {
return false
} else {
return true
}
}
func checkID(s string) bool {
maxL := 64 - len(botName)
if len(s) < 1 || len(s) > maxL {
return false
}
if _, err := strconv.Atoi(s[:1]); err == nil {
return false
}
if strings.Contains(s, "__") {
return false
}
if strings.Contains(s, " ") {
return false
}
//Telegram does not allow sticker name having the word "telegram"
if strings.Contains(s, "telegram") {
return false
}
return true
}
func secHex(n int) string {
bytes := make([]byte, n)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
// func secNum(n int) string {
// numbers := ""
// for i := 0; i < n; i++ {
// randInt, _ := rand.Int(rand.Reader, big.NewInt(10))
// numbers += randInt.String()
// }
// return numbers
// }
func findLink(s string) string {
rx := xurls.Strict()
return rx.FindString(s)
}
func findLinkWithType(s string) (string, string) {
rx := xurls.Strict()
link := rx.FindString(s)
if link == "" {
return "", ""
}
u, _ := url.Parse(link)
host := u.Host
if host == "t.me" {
host = LINK_TG
} else if strings.HasSuffix(host, "line.me") {
host = LINK_IMPORT
} else if strings.HasSuffix(host, "kakao.com") {
host = LINK_IMPORT
}
log.Debugf("link found within findLinkWithType: link=%s, host=%s", link, host)
return link, host
}
func findEmojis(s string) string {
out, err := exec.Command("msb_emoji.py", "string", s).Output()
if err != nil {
return ""
}
return string(out)
}
func findEmojiList(s string) []string {
out, err := exec.Command("msb_emoji.py", "json", s).Output()
if err != nil {
return []string{}
}
list := []string{}
json.Unmarshal(out, &list)
return list
}
func stripEmoji(s string) string {
out, err := exec.Command("msb_emoji.py", "text", s).Output()
if err != nil {
return ""
}
return string(out)
}
func sanitizeCallback(next tele.HandlerFunc) tele.HandlerFunc {
return func(c tele.Context) error {
log.Debug("Sanitizing callback data...")
c.Callback().Data = regexAlphanum.FindString(c.Callback().Data)
log.Debugln("now:", c.Callback().Data)
return next(c)
}
}
func autoRespond(next tele.HandlerFunc) tele.HandlerFunc {
return func(c tele.Context) error {
if c.Callback() != nil {
defer c.Respond()
}
return next(c)
}
}
func escapeTagMark(s string) string {
s = strings.ReplaceAll(s, "<", "<")
s = strings.ReplaceAll(s, ">", ">")
return s
}
func getSIDFromMessage(m *tele.Message) string {
if m.Sticker != nil {
return m.Sticker.SetName
}
link := findLink(m.Text)
return path.Base(link)
}
func retrieveSSDetails(c tele.Context, id string, sd *StickerData) error {
ss, err := c.Bot().StickerSet(id)
if err != nil {
return err
}
sd.stickerSet = ss
sd.title = ss.Title
sd.id = ss.Name
sd.cAmount = len(ss.Stickers)
sd.stickerSetType = ss.Type
if ss.Type == tele.StickerCustomEmoji {
sd.isCustomEmoji = true
}
return nil
}
func GetUd(uidS string) (*UserData, error) {
uid, err := strconv.ParseInt(uidS, 10, 64)
if err != nil {
return nil, err
}
ud, ok := users.data[uid]
if ok {
return ud, nil
} else {
return nil, errors.New("no such user in state")
}
}
func sliceMove[T any](oldIndex int, newIndex int, slice []T) []T {
orig := slice
element := slice[oldIndex]
if oldIndex > newIndex {
if len(slice)-1 == oldIndex {
slice = slice[0 : len(slice)-1]
} else {
slice = append(slice[0:oldIndex], slice[oldIndex+1:]...)
}
slice = append(slice[:newIndex], append([]T{element}, slice[newIndex:]...)...)
} else if oldIndex < newIndex {
slice = append(slice[0:oldIndex], slice[oldIndex+1:]...)
if newIndex != len(slice) {
newIndex = newIndex + 1
}
slice = append(slice[:newIndex], append([]T{element}, slice[newIndex:]...)...)
} else {
return orig
}
return slice
}
func chunkSlice(slice []string, chunkSize int) [][]string {
var chunks [][]string
for {
if len(slice) == 0 {
break
}
if len(slice) < chunkSize {
chunkSize = len(slice)
}
chunks = append(chunks, slice[0:chunkSize])
slice = slice[chunkSize:]
}
return chunks
}
func compCRC32(f1 string, f2 string) bool {
fb1, err := os.ReadFile(f1)
if err != nil {
return false
}
fb2, err := os.ReadFile(f2)
if err != nil {
return false
}
c1 := crc32.ChecksumIEEE(fb1)
c2 := crc32.ChecksumIEEE(fb2)
log.Debugf("File:%s, C:%v", f1, c1)
log.Debugf("File:%s, C:%v", f2, c2)
if c1 == c2 {
return true
} else {
return false
}
}
// func hashCRC64(s string) string {
// h := crc64.New(crc64.MakeTable(crc64.ISO))
// h.Write([]byte(s))
// csum := fmt.Sprintf("%x", h.Sum(nil))
// return csum
// }
func checkGnerateSIDFromLID(ld *msbimport.LineData) string {
id := ld.Id
id = strings.ReplaceAll(id, "-", "_")
id = strings.ReplaceAll(id, "__", "_")
s := ld.Store + id + secHex(2) + "_by_" + botName
if len(s) > 64 {
log.Debugln("id too long:", len(s))
extra := len(s) - 64
id = id[:len(id)-extra]
s = ld.Store + id + secHex(2) + "_by_" + botName
s = strings.ReplaceAll(s, "__", "_")
log.Debugln("Shortend id to:", s)
}
return s
}
// // Local bot api returns a absolute path in FilePath.
// // We need to separate "real" api server and local api server.
// // We move the file from api server to target location.
// // Be careful, this does not work when crossing mount points.
// func teleDownload(tf *tele.File, f string) error {
// // if msbconf.BotApiAddr != "" {
// // tf2, err := b.FileByID(tf.FileID)
// // if err != nil {
// // return err
// // }
// // err = os.Rename(tf2.FilePath, f)
// // if err != nil {
// // exec.Command("cp", tf2.FilePath, f).CombinedOutput()
// // }
// // return os.Chmod(f, 0644)
// // } else {
// return b.Download(tf, f)
// // }
// }
// To comply with new InputSticker requirement on format,
// guess format based on file extension.
func guessInputStickerFormat(f string) string {
if strings.HasSuffix(f, ".webm") {
return "video"
} else {
return "static"
}
}
================================================
FILE: core/webapp.go
================================================
package core
import (
"crypto/hmac"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/panjf2000/ants/v2"
log "github.com/sirupsen/logrus"
tele "gopkg.in/telebot.v3"
)
func InitWebAppServer() {
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
u, err := url.Parse(msbconf.WebappUrl)
if err != nil {
log.Error("Failed parsing WebApp URL! Consider disable --webapp ?")
log.Fatalln(err.Error())
}
p := u.Path
webappApi := r.Group(path.Join(p, "api"))
{
//Group: /webapp/api
webappApi.POST("/initData", apiInitData)
webappApi.GET("/ss", apiSS)
webappApi.POST("/edit/result", apiEditResult)
webappApi.POST("/edit/move", apiEditMove)
webappApi.GET("/export", apiExport)
}
go func() {
err := r.Run(msbconf.WebappApiListenAddr)
if err != nil {
log.Fatalln("WebApp: Gin Run failed! Check your addr or disable webapp.\n", err)
}
log.Infoln("WebApp: Listening on ", msbconf.WebappApiListenAddr)
}()
}
func apiExport(c *gin.Context) {
sn := c.Query("sn")
qid := c.Query("qid")
hex := c.Query("hex")
dn := c.Query("dn")
url := fmt.Sprintf("msb://app/export/%s/?dn=%s&qid=%s&hex=%s", sn, dn, qid, hex)
c.Redirect(http.StatusFound, url)
}
type webappStickerSet struct {
//Sticker objects
SS []webappStickerObject `json:"ss"`
//StickerSet Name
SSName string `json:"ssname"`
//StickerSet Title
SSTitle string `json:"sstitle"`
//StickerSet PNG Thumbnail
SSThumb string `json:"ssthumb"`
//Is Animated WebP
Animated bool `json:"animated"`
Amount int `json:"amount"`
//Indicates that all sticker files are ready
Ready bool `json:"ready"`
}
type webappStickerObject struct {
//Sticker index with offset of +1
Id int `json:"id"`
//Sticker emojis.
Emoji string `json:"emoji"`
//Sticker emoji changed on front-end.
EmojiChanged bool `json:"emoji_changed"`
//Sticker file path on server.
FilePath string `json:"file_path"`
//Sticker file ID
FileID string `json:"file_id"`
//Sticker unique ID
UniqueID string `json:"unique_id"`
//URL of sticker image.
Surl string `json:"surl"`
}
// GET <- ?uid&qid&sn&cmd
// -------------------------------------------
// -> [webappStickerObject, ...]
// -------------------------------------------
// id starts from 1 !!!!
// surl might be 404 when preparing stickers.
func apiSS(c *gin.Context) {
cmd := c.Query("cmd")
sn := c.Query("sn")
uid := c.Query("uid")
qid := c.Query("qid")
hex := c.Query("hex")
var ss *tele.StickerSet
var err error
switch cmd {
case "edit":
ud, err := checkGetUd(uid, qid)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
// Refresh SS data since it might already changed.
retrieveSSDetails(ud.lastContext, ud.stickerData.id, ud.stickerData)
ss = ud.stickerData.stickerSet
case "export":
if sn == "" || qid == "" {
c.String(http.StatusBadRequest, "no_sn_or_qid")
return
}
ss, err = b.StickerSet(sn)
if err != nil {
c.String(http.StatusBadRequest, "bad_sn")
return
}
default:
c.String(http.StatusBadRequest, "no_cmd")
return
}
wss := webappStickerSet{
SSTitle: ss.Title,
SSName: ss.Name,
}
sl := []webappStickerObject{}
ready := true
for i, s := range ss.Stickers {
var surl string
var fpath string
if s.Video {
fpath = filepath.Join(msbconf.WebappDataDir, hex, s.SetName, s.UniqueID+".webm")
} else {
fpath = filepath.Join(msbconf.WebappDataDir, hex, s.SetName, s.UniqueID+".webp")
}
surl, _ = url.JoinPath(msbconf.WebappUrl, "data", hex, s.SetName, s.UniqueID+".webp")
sl = append(sl, webappStickerObject{
Id: i + 1,
Emoji: s.Emoji,
Surl: surl,
UniqueID: s.UniqueID,
FileID: s.FileID,
FilePath: fpath,
})
if i == 0 {
wss.SSThumb, _ = url.JoinPath(msbconf.WebappUrl, "data", hex, s.SetName, s.UniqueID+".png")
}
if st, _ := os.Stat(fpath); st == nil {
ready = false
}
}
wss.SS = sl
wss.Ready = ready
jsonWSS, err := json.Marshal(wss)
if err != nil {
log.Errorln("json marshal jsonWSS in apiSS error!")
c.String(http.StatusInternalServerError, "json marshal jsonWSS in apiSS error!")
return
}
c.String(http.StatusOK, string(jsonWSS))
}
// <- ?qid&qid&sha256sum [{"index", "emoji", "surl"}, ...]
// -------------------------------------------
// -> STATUS
func apiEditResult(c *gin.Context) {
uid := c.Query("uid")
qid := c.Query("qid")
body, _ := io.ReadAll(c.Request.Body)
// if !validateSHA256(body, sum) {
// c.String(http.StatusBadRequest, "bad result csum!")
// return
// }
if string(body) == "" {
//user did nothing
return
}
so := []webappStickerObject{}
err := json.Unmarshal(body, &so)
if err != nil {
c.String(http.StatusBadRequest, "bad_json")
return
}
ud, err := checkGetUd(uid, qid)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
if ud.state == ST_PROCESSING {
c.String(http.StatusOK, "already processing...")
return
}
log.Debugln(so)
c.String(http.StatusOK, "")
ud.udSetState(ST_PROCESSING)
go func() {
err := commitEmojiChange(ud, so)
if err != nil {
sendFatalError(err, ud.lastContext)
endManageSession(ud.lastContext)
endSession(ud.lastContext)
}
}()
}
func commitEmojiChange(ud *UserData, so []webappStickerObject) error {
//Wait for previous jobs to be done.
waitTime := 0
for ud.webAppWorkerPool.Waiting() > 0 {
time.Sleep(500 * time.Millisecond)
waitTime++
if waitTime > 20 {
break
}
}
ud.webAppWorkerPool.ReleaseTimeout(10 * time.Second)
//copy slice
ss := ud.stickerData.stickerSet.Stickers
for i, s := range so {
if s.EmojiChanged {
oldEmoji := findEmojis(ss[i].Emoji)
newEmoji := findEmojis(s.Emoji)
newEmojiList := findEmojiList(s.Emoji)
if newEmoji == "" || newEmoji == oldEmoji {
log.Info("webapp: ignored one invalid emoji.")
continue
}
log.Debugln("Old:", i, s.Emoji, s.FileID)
log.Debugln("New", i, newEmoji)
err := editStickerEmoji(newEmojiList, s.FileID, ud)
if err != nil {
return err
}
}
}
sendSEditOK(ud.lastContext)
return nil
}
// <- ?uid&qid POST_FORM:{"oldIndex", "newIndex"}
// -------------------------------------------
// -> STATUS
func apiEditMove(c *gin.Context) {
uid := c.Query("uid")
qid := c.Query("qid")
oldIndex, _ := strconv.Atoi(c.PostForm("oldIndex"))
newIndex, _ := strconv.Atoi(c.PostForm("newIndex"))
ud, err := checkGetUd(uid, qid)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
smo := &StickerMoveObject{
wg: sync.WaitGroup{},
sd: ud.stickerData,
oldIndex: oldIndex,
newIndex: newIndex,
}
smo.wg.Add(1)
ud.webAppWorkerPool.Invoke(smo)
smo.wg.Wait()
if smo.err != nil {
c.String(http.StatusInternalServerError, smo.err.Error())
return
}
}
func apiInitData(c *gin.Context) {
//We must verify the initData before using it
queryID := c.PostForm("query_id")
authDate := c.PostForm("auth_date")
user := c.PostForm("user")
hash := c.PostForm("hash")
dataCheckString := strings.Join([]string{
"auth_date=" + authDate,
"query_id=" + queryID,
"user=" + user}, "\n")
if !validateHMAC(dataCheckString, hash) {
log.Warning("WebApp DCS HMAC failed, corrupt or attack?")
c.String(http.StatusBadRequest, "data_check_string HMAC validation failed!!")
return
}
log.Debug("WebApp initData DCS HMAC OK.")
initWebAppRequest(c)
}
func initWebAppRequest(c *gin.Context) {
user := c.PostForm("user")
queryID := c.PostForm("query_id")
cmd := c.Query("cmd")
cmd = path.Base(cmd)
webAppUser := &WebAppUser{}
err := json.Unmarshal([]byte(user), webAppUser)
if err != nil {
log.Error("json unmarshal webappuser error.")
return
}
switch cmd {
case "edit":
ud, err := GetUd(strconv.Itoa(webAppUser.Id))
if err != nil {
c.String(http.StatusBadRequest, "bad_state")
return
}
ud.webAppWorkerPool, _ = ants.NewPoolWithFunc(1, wSubmitSMove)
ud.webAppQID = queryID
case "export":
sn := c.Query("sn")
hex := c.Query("hex")
if sn == "" || hex == "" {
c.String(http.StatusBadRequest, "no_sn_or_hex")
return
}
ss, err := b.StickerSet(sn)
if err != nil {
c.String(http.StatusBadRequest, "bad_sn")
return
}
// appendSStoQIDAuthList(sn, queryID)
prepareWebAppExportStickers(ss, hex)
default:
c.String(http.StatusBadRequest, "bad_or_no_cmd")
return
}
c.String(http.StatusOK, "webapp init ok")
}
// Telegram WebApp Regulation.
func validateHMAC(dataCheckString string, hash string) bool {
// This calculated secret will be used to "decrypt" DCS
h := hmac.New(sha256.New, []byte("WebAppData"))
h.Write([]byte(msbconf.BotToken))
secretByte := h.Sum(nil)
h = hmac.New(sha256.New, secretByte)
h.Write([]byte(dataCheckString))
dcsHash := fmt.Sprintf("%x", h.Sum(nil))
return hash == dcsHash
}
// func validateSHA256(dataToCheck []byte, hash string) bool {
// h := sha256.New()
// h.Write(dataToCheck)
// csum := fmt.Sprintf("%x", h.Sum(nil))
// return hash == csum
// }
func checkGetUd(uid string, qid string) (*UserData, error) {
ud, err := GetUd(uid)
if err != nil {
return nil, errors.New("no such user")
}
if ud.webAppQID != qid {
return nil, errors.New("qid not valid")
}
return ud, nil
}
func prepareWebAppEditStickers(ud *UserData) error {
dest := filepath.Join(msbconf.WebappDataDir, ud.stickerData.id)
os.RemoveAll(dest)
os.MkdirAll(dest, 0755)
for _, s := range ud.stickerData.stickerSet.Stickers {
var f string
if s.Video {
f = filepath.Join(dest, s.UniqueID+".webm")
} else {
f = filepath.Join(dest, s.UniqueID+".webp")
}
obj := &StickerDownloadObject{
dest: f,
sticker: s,
forWebApp: true,
}
obj.wg.Add(1)
ud.stickerData.sDnObjects = append(ud.stickerData.sDnObjects, obj)
go wpDownloadStickerSet.Invoke(obj)
}
return nil
}
func prepareWebAppExportStickers(ss *tele.StickerSet, hex string) error {
dest := filepath.Join(msbconf.WebappDataDir, hex, ss.Name)
// If the user is reusing the generated link to export.
// Do not re-download for every initData.
stat, _ := os.Stat(dest)
if stat != nil {
mtime := stat.ModTime().Unix()
// Less than 5 minutes, do not re-download
if time.Now().Unix()-mtime < 600 {
log.Debug("prepareWebAppExportStickers: dir still fresh, don't overwrite.")
return nil
}
}
os.RemoveAll(dest)
os.MkdirAll(dest, 0755)
for i, s := range ss.Stickers {
var f string
if s.Video {
f = filepath.Join(dest, s.UniqueID+".webm")
} else if s.Animated {
f = filepath.Join(dest, s.UniqueID+".tgs")
} else {
f = filepath.Join(dest, s.UniqueID+".webp")
}
obj := &StickerDownloadObject{
dest: f,
sticker: s,
// forWebApp: true,
forWhatsApp: true,
}
//Use first image to create a thumbnail image
//for WhatsApp.
if i == 0 {
obj.forWhatsAppThumb = true
}
obj.wg.Add(1)
go wpDownloadStickerSet.Invoke(obj)
}
return nil
}
================================================
FILE: core/workers.go
================================================
package core
import (
"strings"
"github.com/panjf2000/ants/v2"
log "github.com/sirupsen/logrus"
"github.com/star-39/moe-sticker-bot/pkg/msbimport"
)
// Workers pool for converting webm
var wpDownloadStickerSet *ants.PoolWithFunc
func initWorkersPool() {
// wpConvertWebm, _ = ants.NewPoolWithFunc(4, wConvertWebm)
wpDownloadStickerSet, _ = ants.NewPoolWithFunc(
8, wDownloadStickerObject)
}
// *StickerDownloadObject
func wDownloadStickerObject(i interface{}) {
obj := i.(*StickerDownloadObject)
defer obj.wg.Done()
log.Debugf("Downloading in pool: %s -> %s", obj.sticker.FileID, obj.dest)
if obj.forWebApp || obj.forWhatsApp {
err := b.Download(&obj.sticker.File, obj.dest)
if err != nil {
log.Warnln("download: error downloading sticker:", err)
obj.err = err
return
}
if obj.forWhatsApp {
if obj.sticker.Video {
obj.err = msbimport.FFToAnimatedWebpWA(obj.dest)
} else {
obj.err = msbimport.IMToWebpWA(obj.dest)
}
if obj.forWhatsAppThumb {
if obj.sticker.Animated {
f := strings.ReplaceAll(obj.dest, ".tgs", ".webp")
obj.err = msbimport.IMToPNGThumb(f)
} else {
obj.err = msbimport.IMToPNGThumb(obj.dest)
}
}
} else {
//TGS set is not managable, no need to convert.
if obj.sticker.Video {
obj.err = msbimport.FFToAnimatedWebpLQ(obj.dest)
}
}
return
}
var f string
var cf string
var err error
if obj.sticker.Video {
f = obj.dest + ".webm"
err = b.Download(&obj.sticker.File, f)
if obj.needConvert {
cf, _ = msbimport.FFToGif(f)
}
} else if obj.sticker.Animated {
f = obj.dest + ".tgs"
err = b.Download(&obj.sticker.File, f)
if obj.needConvert {
cf, _ = msbimport.RlottieToGIF(f)
}
} else {
f = obj.dest + ".webp"
err = b.Download(&obj.sticker.File, f)
if obj.needConvert {
cf, _ = msbimport.IMToPng(f)
}
}
if err != nil {
log.Warnln("download: error downloading sticker:", err)
obj.err = err
return
}
obj.of = f
obj.cf = cf
}
// *StickerMoveObject
func wSubmitSMove(i interface{}) {
obj := i.(*StickerMoveObject)
defer obj.wg.Done()
sid := obj.sd.stickerSet.Stickers[obj.oldIndex].FileID
log.Debugf("Moving in pool %d(%s) -> %d", obj.oldIndex, sid, obj.newIndex)
err := b.SetStickerPosition(sid, obj.newIndex)
if err != nil {
log.Errorln("SMove failed!!", err)
obj.err = err
} else {
log.Debugf("Sticker move OK for %s", obj.sd.stickerSet.Name)
obj.sd.stickerSet.Stickers =
sliceMove(obj.oldIndex, obj.newIndex, obj.sd.stickerSet.Stickers)
}
}
================================================
FILE: deployments/kubernetes_msb.yaml
================================================
# Save the output of this file and use kubectl create -f to import
# it into Kubernetes.
#
# Created with podman-4.2.0
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: "2022-12-09T17:58:22Z"
labels:
app: p-moe-sticker-bot
name: p-moe-sticker-bot
spec:
containers:
- args:
- nginx
- -g
- daemon off;
env:
- name: NGINX_PORT
value: "443"
- name: NGINX_CERT
value: /certs/live/YOUR_WEBSITE/fullchain.pem
- name: WEBAPP_ROOT
value: /webapp
- name: WEBAPP_ADDR
value: 127.0.0.1:3921/webapp
- name: NGINX_KEY
value: /certs/live/YOUR_WEBSITE/privkey.pem
image: ghcr.io/star-39/moe-sticker-bot:msb_nginx_aarch64
name: msbnginx
ports:
- containerPort: 443
hostPort: 443
resources: {}
securityContext:
capabilities:
drop:
- CAP_MKNOD
- CAP_AUDIT_WRITE
tty: true
volumeMounts:
- mountPath: /certs
name: etc-letsencrypt-host-0
- mountPath: /webapp/data
name: moe-sticker-bot-webapp-data-ed
- command:
- /moe-sticker-bot
- --bot_token=BOT_TOKEN
- --webapp
- --webapp_url
- https://example.com/
- --webapp_data_dir
- /webapp/data/
- --webapp_listen_addr
- 127.0.0.1:3921
- --use_db
- --db_addr
- 10.88.0.1:3306
- --db_user
- root
- --db_pass
- DB_ROOT_PASS
image: ghcr.io/star-39/moe-sticker-bot:aarch64
name: msb
resources: {}
securityContext:
capabilities:
drop:
- CAP_MKNOD
- CAP_AUDIT_WRITE
volumeMounts:
- mountPath: /webapp/data
name: moe-sticker-bot-webapp-data-ed
- args:
- mariadbd
env:
- name: MARIADB_ROOT_PASSWORD
value: DB_ROOT_PASS
image: docker.io/library/mariadb:10.6
name: msbmariadb
resources: {}
securityContext:
capabilities:
drop:
- CAP_MKNOD
- CAP_AUDIT_WRITE
volumeMounts:
- mountPath: /var/lib/mysql
name: moe-sticker-bot-db-pvc
hostname: p-moe-sticker-bot
volumes:
- hostPath:
path: /etc/letsencrypt
type: Directory
name: etc-letsencrypt-host-0
- name: moe-sticker-bot-webapp-data-ed
emptyDir: {}
- name: moe-sticker-bot-db-pvc
persistentVolumeClaim:
claimName: moe-sticker-bot-db-pvc
status: {}
================================================
FILE: go.mod
================================================
module github.com/star-39/moe-sticker-bot
go 1.19
replace gopkg.in/telebot.v3 => github.com/star-39/telebot/v3 v3.99.9
replace github.com/armon/go-metrics => github.com/hashicorp/go-metrics v0.5.3
replace github.com/circonus-labs/circonusllhist => github.com/openhistogram/circonusllhist v0.4.0
replace gopkg.in/alecthomas/kingpin.v2 => github.com/alecthomas/kingpin/v2 v2.4.0
//replace gopkg.in/telebot.v3 => /media/ssd/repos/telebot
require (
github.com/PuerkitoBio/goquery v1.9.2
github.com/gin-gonic/gin v1.10.0
github.com/go-co-op/gocron v1.37.0
github.com/go-sql-driver/mysql v1.8.1
github.com/panjf2000/ants/v2 v2.9.1
github.com/sirupsen/logrus v1.9.3
gopkg.in/telebot.v3 v3.2.1
mvdan.cc/xurls/v2 v2.5.0
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/knz/go-libedit v1.10.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-metrics v0.5.3/go.mod h1:KEjodfebIOuBYSAe/bHTm+HChmKSxAOXPBieMLYozDE=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.3.0 h1:jX8FDLfW4ThVXctBNZ+3cIWnCSnrACDV73r76dy0aQQ=
github.com/leodido/go-urn v1.3.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/openhistogram/circonusllhist v0.4.0/go.mod h1:PfeYJ/RW2+Jfv3wTz0upbY2TRour/LLqIm2K2Kw5zg0=
github.com/panjf2000/ants/v2 v2.9.0 h1:SztCLkVxBRigbg+vt0S5QvF5vxAbxbKt09/YfAJ0tEo=
github.com/panjf2000/ants/v2 v2.9.0/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I=
github.com/panjf2000/ants/v2 v2.9.1 h1:Q5vh5xohbsZXGcD6hhszzGqB7jSSc2/CRr3QKIga8Kw=
github.com/panjf2000/ants/v2 v2.9.1/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw=
github.com/star-39/telebot/v3 v3.99.7 h1:hkzOqAwksn7yMo7LxyhmOSqwBxo3PFk7vRbhGcArhcE=
github.com/star-39/telebot/v3 v3.99.7/go.mod h1:Ao7cUAFKISN5qXdy7s32HTjE8yJfKSV0FDhaW3+Qw74=
github.com/star-39/telebot/v3 v3.99.8 h1:/AOYaKvaUsDl7wVle5PIu/+HLjeapobiAhSF8c8RCyc=
github.com/star-39/telebot/v3 v3.99.8/go.mod h1:Ao7cUAFKISN5qXdy7s32HTjE8yJfKSV0FDhaW3+Qw74=
github.com/star-39/telebot/v3 v3.99.9 h1:0AzpYI796wpcEw4+aT0dHoFB/x0LStdqfpggVyZPoOE=
github.com/star-39/telebot/v3 v3.99.9/go.mod h1:Ao7cUAFKISN5qXdy7s32HTjE8yJfKSV0FDhaW3+Qw74=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU=
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
================================================
FILE: pkg/msbimport/README.md
================================================
# Moe-Sticker-Bot Import Component (msbimport)
[](https://pkg.go.dev/github.com/star-39/moe-sticker-bot/pkg/msbimport)
## Description
This package is intended to fetch, parse, download and convert LINE and KakaoTalk Stickers from share link.
It is designed to be able to operate independentaly from moe-sticker-bot core so third party apps can also utilize this package.
此套件或CLI用於解析LINE/Kakaotalk貼圖的分享連結並下載和轉換。
此套件可獨立於moe-sticker-bot使用, 第三方App可以輕鬆利用此套件或CLI處理複雜貼圖。
## CLI Usage/終端機程式使用
Source code of CLI is on: [/moe-sticker-bot/cmd/msbimport](https://github.com/star-39/moe-sticker-bot/tree/master/cmd/msbimport)
Download `msbimport`: 下載`msbimport`: https://github.com/star-39/moe-sticker-bot/releases
Install dependencies:
```bash
# For Fedora / RHEL / CentOS etc. (Requires RPM Fusion)
dnf install ImageMagick libwebp bsdtar curl ffmpeg gifsicle
# For Debian / Ubuntu etc.
apt install imagemagick libarchive-tools curl ffmpeg gifsicle
# For Arch
pacman -S install ffmpeg imagemagick curl libarchive gifsicle bsdtar
# For macOS
brew install imagemagick ffmpeg curl bsdtar gifsicle
install tools/msb_kakao_decrypt.py /usr/local/bin/
install tools/msb_rlottie.py /usr/local/bin/
```
```bash
msbimport --help
Usage of ./msbimport:
-convert
Convert to Telegram format(WEBP/WEBM) 轉換為Telegram格式。
-dir string
Where to put sticker files. 指定存放貼圖檔的資料夾。
-json
Output JSON serialized LineData, useful when integrating with other apps. 列印LineData為JSON。
-link string
Import link(LINE/kakao) 分享連結。
-log_level string
Log level (default "debug")
msbimport --link https://store.line.me/stickershop/product/27286
msbimport --link https://store.line.me/stickershop/product/27286 --convert --json
```
## API Usage
A typical workflow is to call `parseImportLink` then `prepareImportStickers`.
```
go get -u https://github.com/star-39/moe-sticker-bot
```
```go
import "github.com/star-39/moe-sticker-bot/pkg/msbimport"
//Create a context, which can be used to interrupt the process.
ctx, _ := context.WithCancel(context.Background())
//Create a empty LineData struct pointer.
ld := &msbimport.LineData{}
//LineData will be parsed to ld.
warn, err := msbimport.ParseImportLink("https://store.line.me/stickershop/product/27286", ld)
if err != nil {
//handle error here.
}
if warn != "" {
//handle warning message here.
}
err = msbimport.PrepareImportStickers(ctx, ld, "./", false)
if err != nil {
//handle error here.
}
//If I18n title is needed(LINE only), TitleWg must be waited.
ld.TitleWg.Wait()
println(ld.I18nTitles)
for _, lf := range ld.Files {
//Each file has its own waitgroup and musted be waited.
lf.Wg.Wait()
if lf.CError != nil {
//hanlde sticker error here.
}
println(lf.OriginalFile)
println(lf.ConvertedFile)
//...
}
//Your stickers files will appear in the work dir you specified.
```
## License
GPL v3 License.
Source code of this package MUST ALWAYS be disclosed no matter what use case is,
and source code referring to this package MUST ALSO be discolsed and share the same GPL v3 License.
================================================
FILE: pkg/msbimport/convert.go
================================================
package msbimport
import (
"errors"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"time"
log "github.com/sirupsen/logrus"
)
var FFMPEG_BIN = "ffmpeg"
var BSDTAR_BIN = "bsdtar"
var CONVERT_BIN = "convert"
var IDENTIFY_BIN = "identify"
var CONVERT_ARGS []string
var IDENTIFY_ARGS []string
const (
FORMAT_TG_REGULAR_STATIC = "tg_reg_static"
FORMAT_TG_EMOJI_STATIC = "tg_emoji_static"
FORMAT_TG_REGULAR_ANIMATED = "tg_reg_ani"
FORMAT_TG_EMOJI_ANIMATED = "tg_emoji_ani"
)
// See: http://en.wikipedia.org/wiki/Binary_prefix
const (
// Decimal
KB = 1000
MB = 1000 * KB
GB = 1000 * MB
TB = 1000 * GB
PB = 1000 * TB
// Binary
KiB = 1024
MiB = 1024 * KiB
GiB = 1024 * MiB
TiB = 1024 * GiB
PiB = 1024 * TiB
)
// Should call before using functions in this package.
// Otherwise, defaults to Linux environment.
// This function also call CheckDeps to check if executables.
func InitConvert() {
switch runtime.GOOS {
case "linux":
CONVERT_BIN = "convert"
default:
CONVERT_BIN = "magick"
IDENTIFY_BIN = "magick"
CONVERT_ARGS = []string{"convert"}
IDENTIFY_ARGS = []string{"identify"}
}
unfoundBins := CheckDeps()
if len(unfoundBins) != 0 {
log.Warning("Following required executables not found!:")
log.Warnln(strings.Join(unfoundBins, " "))
log.Warning("Please install missing executables to your PATH, or some features will not work!")
}
}
// Check if required dependencies exist and return a string slice
// containing binaries that are not found in PATH.
func CheckDeps() []string {
unfoundBins := []string{}
if _, err := exec.LookPath(FFMPEG_BIN); err != nil {
unfoundBins = append(unfoundBins, FFMPEG_BIN)
}
if _, err := exec.LookPath(BSDTAR_BIN); err != nil {
unfoundBins = append(unfoundBins, BSDTAR_BIN)
}
if _, err := exec.LookPath(CONVERT_BIN); err != nil {
unfoundBins = append(unfoundBins, CONVERT_BIN)
}
if _, err := exec.LookPath("exiv2"); err != nil {
unfoundBins = append(unfoundBins, "exiv2")
}
if _, err := exec.LookPath("gifsicle"); err != nil {
unfoundBins = append(unfoundBins, "gifsicle")
}
return unfoundBins
}
// Convert any image to static WEBP image, for Telegram use.
// `format` takes either FORMAT_TG_REGULAR_STATIC or FORMAT_TG_EMOJI_STATIC
func IMToWebpTGStatic(f string, isCustomEmoji bool) (string, error) {
pathOut := f + ".webp"
bin := CONVERT_BIN
args := CONVERT_ARGS
if isCustomEmoji {
args = append(args, "-resize", "100x100", "-gravity", "center", "-extent", "100x100", "-background", "none")
} else {
args = append(args, "-resize", "512x512")
}
args = append(args, "-filter", "Lanczos", "-define", "webp:lossless=true", f+"[0]", pathOut)
out, err := exec.Command(bin, args...).CombinedOutput()
if err != nil {
log.Warnln("IMToWebpTGRegular ERROR:", string(out))
return "", err
}
st, err := os.Stat(pathOut)
if err != nil {
return "", err
}
// 100x100 should never exceed 255KIB, no need for extra check.
if st.Size() > 255*KiB {
args := CONVERT_ARGS
args = append(args, "-resize", "512x512", "-filter", "Lanczos", f+"[0]", pathOut)
exec.Command(bin, args...).CombinedOutput()
}
return pathOut, err
}
// Convert image to static Webp for Whatsapp, size limit is 100KiB.
func IMToWebpWA(f string) error {
pathOut := f
bin := CONVERT_BIN
args := CONVERT_ARGS
qualities := []string{"75", "50"}
for _, q := range qualities {
args = append(args, "-define", "webp:quality="+q,
"-resize", "512x512", "-gravity", "center", "-extent", "512x512",
"-background", "none", f+"[0]", pathOut)
out, err := exec.Command(bin, args...).CombinedOutput()
if err != nil {
log.Warnln("imToWebp ERROR:", string(out))
return err
}
st, err := os.Stat(pathOut)
if err != nil {
return err
}
if st.Size() > 100*KiB {
continue
} else {
return nil
}
}
return errors.New("bad webp")
}
func IMToPng(f string) (string, error) {
pathOut := f + ".png"
bin := CONVERT_BIN
args := CONVERT_ARGS
args = append(args, f, pathOut)
out, err := exec.Command(bin, args...).CombinedOutput()
if err != nil {
log.Warnln("imToPng ERROR:", string(out))
return "", err
}
return pathOut, err
}
func IMToApng(f string) (string, error) {
pathOut := f + ".apng"
bin := CONVERT_BIN
args := CONVERT_ARGS
args = append(args, f, pathOut)
out, err := exec.Command(bin, args...).CombinedOutput()
if err != nil {
log.Warnln("imToApng ERROR:", string(out))
return "", err
}
return pathOut, err
}
// If the source is IMAGE, convert to WEBP,
// If the source is VIDEO, convert to WEBM
func ConverMediaToTGStickerSmart(f string, isCustomEmoji bool) (string, error) {
var isVideo bool
//Determine whether the media is Video or Image by counting frames.
identifyBin := IDENTIFY_BIN
identifyArgs := IDENTIFY_ARGS
identifyArgs = append(identifyArgs, "-format", "%n", f)
identifyOut, err := exec.Command(identifyBin, identifyArgs...).CombinedOutput()
if err != nil {
log.Warnln("ConverMediaToTGStickerSmart ERROR:", string(identifyOut))
return "", err
}
//IM might get buggy and return insane frame count causing overflow.
//Trim it.
identifyOutString := string(identifyOut)
if len(identifyOutString) > 5 {
identifyOutString = identifyOutString[:3]
}
frameCount, err := strconv.Atoi(identifyOutString)
if err != nil {
log.Warnln("ConverMediaToTGStickerSmart Atoi ERROR:", err)
return "", err
}
if frameCount > 1 {
isVideo = true
} else if frameCount == 0 {
log.Warnln("ConverMediaToTGStickerSmart ERROR: Frame count is zero.")
return "", errors.New("frame count is zero")
}
if isVideo {
return FFToWebmTGVideo(f, isCustomEmoji)
} else {
return IMToWebpTGStatic(f, isCustomEmoji)
}
}
func FFToWebmTGVideo(f string, isCustomEmoji bool) (string, error) {
pathOut := f + ".webm"
bin := FFMPEG_BIN
baseargs := []string{}
baseargs = append(baseargs, "-hide_banner", "-i", f)
if isCustomEmoji {
baseargs = append(baseargs, "-vf", "scale=100:100:force_original_aspect_ratio=decrease")
} else {
baseargs = append(baseargs, "-vf", "scale=512:512:force_original_aspect_ratio=decrease")
}
baseargs = append(baseargs, "-pix_fmt", "yuva420p", "-c:v", "libvpx-vp9", "-cpu-used", "5")
for rc := 0; rc < 4; rc++ {
rcargs := []string{}
switch rc {
case 0:
rcargs = []string{"-minrate", "50k", "-b:v", "350k", "-maxrate", "450k"}
case 1:
rcargs = []string{"-minrate", "50k", "-b:v", "200k", "-maxrate", "300k"}
case 2:
rcargs = []string{"-minrate", "20k", "-b:v", "100k", "-maxrate", "200k"}
case 3:
rcargs = []string{"-minrate", "10k", "-b:v", "50k", "-maxrate", "100k"}
}
args := append(baseargs, rcargs...)
args = append(args, []string{"-to", "00:00:03", "-an", "-y", pathOut}...)
out, err := exec.Command(bin, args...).CombinedOutput()
if err != nil {
log.Warnln("ffToWebm ERROR:", string(out))
//FFMPEG does not support animated webp.
//Convert to APNG first than WEBM.
if strings.Contains(string(out), "skipping unsupported chunk: ANIM") {
log.Warnln("Trying to convert to APNG first.")
f2, _ := IMToApng(f)
return FFToWebmTGVideo(f2, isCustomEmoji)
}
return pathOut, err
}
stat, err := os.Stat(pathOut)
if err != nil {
return pathOut, err
}
if stat.Size() > 255*KiB {
continue
} else {
return pathOut, err
}
}
log.Errorln("FFToWebmTGVideo: unable to compress below 256KiB:", pathOut)
return pathOut, errors.New("FFToWebmTGVideo: unable to compress below 256KiB")
}
// This function will be called if Telegram's API rejected our webm.
// It is normally due to overlength or bad FPS rate.
func FFToWebmSafe(f string, isCustomEmoji bool) (string, error) {
pathOut := f + ".webm"
bin := FFMPEG_BIN
args := []string{}
args = append(args, "-hide_banner", "-i", f)
if isCustomEmoji {
args = append(args, "-vf", "scale=100:100:force_original_aspect_ratio=decrease")
} else {
args = append(args, "-vf", "scale=512:512:force_original_aspect_ratio=decrease:flags=lanczos")
}
args = append(args, "-pix_fmt", "yuva420p",
"-c:v", "libvpx-vp9", "-cpu-used", "5", "-minrate", "50k", "-b:v", "200k", "-maxrate", "300k",
"-to", "00:00:02.800", "-r", "30", "-an", "-y", pathOut)
cmd := exec.Command(bin, args...)
err := cmd.Run()
return pathOut, err
}
func FFToGif(f string) (string, error) {
var decoder []string
var args []string
if strings.HasSuffix(f, ".webm") {
decoder = []string{"-c:v", "libvpx-vp9"}
}
pathOut := f + ".gif"
bin := FFMPEG_BIN
args = append(args, decoder...)
args = append(args, "-i", f, "-hide_banner",
"-lavfi", "split[a][b];[a]palettegen[p];[b][p]paletteuse=dither=atkinson",
"-gifflags", "-transdiff", "-gifflags", "-offsetting",
"-loglevel", "error", "-y", pathOut)
out, err := exec.Command(bin, args...).CombinedOutput()
if err != nil {
log.Warnf("ffToGif ERROR:\n%s", string(out))
return "", err
}
//Optimize GIF produced by ffmpeg
exec.Command("gifsicle", "--batch", "-O2", "--lossy=60", pathOut).CombinedOutput()
return pathOut, err
}
// func FFToAPNG(f string) (string, error) {
// var decoder []string
// var args []string
// if strings.HasSuffix(f, ".webm") {
// decoder = []string{"-c:v", "libvpx-vp9"}
// }
// pathOut := f + ".apng"
// bin := FFMPEG_BIN
// args = append(args, decoder...)
// args = append(args, "-i", f, "-hide_banner",
// "-loglevel", "error", "-y", pathOut)
// out, err := exec.Command(bin, args...).CombinedOutput()
// if err != nil {
// log.Warnf("ffToAPNG ERROR:\n%s", string(out))
// return "", err
// }
// return pathOut, err
// }
func IMStackToWebp(base string, overlay string) (string, error) {
bin := CONVERT_BIN
args := CONVERT_ARGS
fOut := base + ".composite.webp"
args = append(args, base, overlay, "-background", "none", "-filter", "Lanczos", "-resize", "512x512", "-composite",
"-define", "webp:lossless=true", fOut)
out, err := exec.Command(bin, args...).CombinedOutput()
if err != nil {
log.Errorln("IM stack ERROR!", string(out))
return "", err
} else {
return fOut, nil
}
}
// Replaces tgs to gif.
func RlottieToGIF(f string) (string, error) {
bin := "msb_rlottie.py"
fOut := strings.ReplaceAll(f, ".tgs", ".gif")
args := []string{f, fOut}
out, err := exec.Command(bin, args...).CombinedOutput()
if err != nil {
log.Errorln("lottieToGIF ERROR!", string(out))
return "", err
}
//Optimize GIF
exec.Command("gifsicle", "--batch", "-O2", "--lossy=60", fOut).CombinedOutput()
return fOut, nil
}
// Replaces tgs to webp.
// The only purpose for this func is for WhatsApp export.
// func RlottieToWebpWAAnimated(f string) (string, error) {
// bin := "msb_rlottie.py"
// pathOut := strings.ReplaceAll(f, ".tgs", ".webp")
// qualities := []string{"50", "20", "0"}
// for _, q := range qualities {
// args := []string{f, pathOut, q}
// out, err := exec.Command(bin, args...).CombinedOutput()
// if err != nil {
// log.Errorln("RlottieToWebp ERROR!", string(out))
// return "", err
// }
// //WhatsApp uses KiB.
// st, err := os.Stat(pathOut)
// if err != nil {
// return pathOut, err
// }
// if st.Size() > 500*KiB {
// log.Warnf("convert: awebp exceeded 500KiB, q:%s z:%d s:%s", q, st.Size(), pathOut)
// continue
// } else {
// return pathOut, nil
// }
// }
// log.Warnln("all quality failed! s:", pathOut)
// return pathOut, errors.New("bad animated webp?")
// }
// Replaces .webm ext to .webp
func IMToAnimatedWebpLQ(f string) error {
pathOut := strings.ReplaceAll(f, ".webm", ".webp")
bin := CONVERT_BIN
args := CONVERT_ARGS
args = append(args, "-resize", "128x128", f, pathOut)
out, err := exec.Command(bin, args...).CombinedOutput()
if err != nil {
log.Warnln("imToWebp ERROR:", string(out))
return err
}
return err
}
// Replaces .webm ext to .webp
func FFToAnimatedWebpLQ(f string) error {
pathOut := strings.ReplaceAll(f, ".webm", ".webp")
bin := FFMPEG_BIN
args := []string{"-hide_banner", "-c:v", "libvpx-vp9", "-i", f,
"-vf", "scale=128:128:force_original_aspect_ratio=decrease",
"-loop", "0", "-pix_fmt", "yuva420p",
"-an", "-y", pathOut}
out, err := exec.Command(bin, args...).CombinedOutput()
if err != nil {
log.Warnln("ffToAnimatedWebpWA ERROR:", string(out))
return err
}
return nil
}
// // animated webp has a pretty bad compression ratio comparing to VP9,
// // shrink down quality as much as possible.
func FFToAnimatedWebpWA(f string) error {
pathOut := strings.ReplaceAll(f, ".webm", ".webp")
bin := FFMPEG_BIN
//Try qualities from best to worst.
qualities := []string{"75", "50", "20", "0", "_DS256", "_DS256Q0"}
for _, q := range qualities {
args := []string{"-hide_banner", "-c:v", "libvpx-vp9", "-i", f,
"-vf", "scale=512:512:force_original_aspect_ratio=decrease,pad=512:512:-1:-1:color=black@0",
"-quality", q, "-loop", "0", "-pix_fmt", "yuva420p",
"-an", "-y", pathOut}
if q == "_DS256" {
args = []string{"-hide_banner", "-c:v", "libvpx-vp9", "-i", f,
"-vf", "scale=256:256:force_original_aspect_ratio=decrease,pad=512:512:-1:-1:color=black@0",
"-quality", "20", "-loop", "0", "-pix_fmt", "yuva420p",
"-an", "-y", pathOut}
}
if q == "_DS256Q0" {
args = []string{"-hide_banner", "-c:v", "libvpx-vp9", "-i", f,
"-vf", "scale=256:256:force_original_aspect_ratio=decrease,pad=512:512:-1:-1:color=black@0",
"-quality", "0", "-loop", "0", "-pix_fmt", "yuva420p",
"-an", "-y", pathOut}
}
out, err := exec.Command(bin, args...).CombinedOutput()
if err != nil {
log.Warnln("ffToAnimatedWebpWA ERROR:", string(out))
return err
}
//WhatsApp uses KiB.
st, err := os.Stat(pathOut)
if err != nil {
return err
}
if st.Size() > 500*KiB {
log.Warnf("convert: awebp exceeded 500KiB, q:%s z:%d s:%s", q, st.Size(), pathOut)
continue
} else {
return nil
}
}
log.Warnln("all quality failed! s:", pathOut)
return errors.New("bad animated webp?")
}
// appends png
func FFtoPNG(f string, pathOut string) error {
var args []string
bin := FFMPEG_BIN
args = append(args, "-c:v", "libvpx-vp9", "-i", f, "-hide_banner",
"-loglevel", "error", "-frames", "1", "-y", pathOut)
out, err := exec.Command(bin, args...).CombinedOutput()
if err != nil {
log.Warnf("fftoPNG ERROR:\n%s", string(out))
return err
}
return err
}
// Replaces .webm or .webp to .png
func IMToPNGThumb(f string) error {
pathOut := strings.ReplaceAll(f, ".webm", ".png")
pathOut = strings.ReplaceAll(pathOut, ".webp", ".png")
if strings.HasSuffix(f, ".webm") {
tempThumb := f + ".thumb.png"
FFtoPNG(f, tempThumb)
f = tempThumb
}
bin := CONVERT_BIN
args := CONVERT_ARGS
args = append(args,
"-resize", "96x96",
"-gravity", "center", "-extent", "96x96", "-background", "none",
f+"[0]", pathOut)
out, err := exec.Command(bin, args...).CombinedOutput()
if err != nil {
log.Warnln("imToPng ERROR:", string(out))
return err
}
return err
}
func SetImageTime(f string, t time.Time) error {
return os.Chtimes(f, t, t)
// asciiTime := t.Format("2006:01:02 15:04:05")
// _, err := exec.Command("exiv2", "-M", "set Exif.Image.DateTime "+asciiTime, f).CombinedOutput()
// if err != nil {
// return err
// }
// return nil
}
================================================
FILE: pkg/msbimport/import.go
================================================
package msbimport
import (
"context"
"errors"
"net/url"
"strings"
log "github.com/sirupsen/logrus"
)
// This function serves as an entrypoint for this package.
// Parse a LINE or Kakao link and fetch metadata.
// The metadata (which means the LineData struct) can be used to call prepareImportStickers.
// Returns a string and an error. String act as a warning message, empty string means no warning yield.
//
// Attention: After this function returns, ld.Amount, ld.Files will NOT be available!
func ParseImportLink(link string, ld *LineData) (string, error) {
var warn string
u, err := url.Parse(link)
if err != nil {
return warn, err
}
switch {
case strings.HasSuffix(u.Host, "line.me"):
ld.Store = "line"
return parseLineLink(link, ld)
case strings.HasSuffix(u.Host, "kakao.com"):
ld.Store = "kakao"
return parseKakaoLink(link, ld)
default:
return warn, errors.New("unknow import")
}
}
// Prepare stickers files.
// Should be called after calling ParseImportLink().
// A context is provided, which can be used to interrupt the process.
// Even if this function returns, file preparation might still in progress.
// LineData.Amount, LineData.Files will be produced after return.
// wg.Wait() is required for individual LineData.Files
//
// convertToTGFormat: Convert original stickers to Telegram sticker format.
// convertToTGEmoji: If present sticker set is Emoji(LINE), convert to 100x100 Telegram CustomEmoji.
func PrepareImportStickers(ctx context.Context, ld *LineData, workDir string, convertToTGFormat bool, convertToTGEmoji bool) error {
switch ld.Store {
case "line":
return prepareLineStickers(ctx, ld, workDir, convertToTGFormat, convertToTGEmoji)
case "kakao":
return prepareKakaoStickers(ctx, ld, workDir, convertToTGFormat)
}
return nil
}
// Convert imported sticker to Telegram format,
// which means WEBM for animated and WEBP for static
// with 512x512 dimension.
func convertSToTGFormat(ctx context.Context, ld *LineData) {
for _, s := range ld.Files {
select {
case <-ctx.Done():
log.Warn("convertSToTGFormat received ctxDone!")
return
default:
}
var err error
// If lineS is animated, commit to worker pool
// since encoding vp9 is time and resource costy.
if ld.IsAnimated {
wpConvertWebm.Invoke(s)
} else {
s.ConvertedFile, err = IMToWebpTGStatic(s.OriginalFile, s.ConvertToEmoji)
if err != nil {
s.CError = err
}
s.Wg.Done()
}
}
}
================================================
FILE: pkg/msbimport/import_kakao.go
================================================
package msbimport
import (
"context"
"encoding/json"
"errors"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"github.com/PuerkitoBio/goquery"
log "github.com/sirupsen/logrus"
)
func parseKakaoLink(link string, ld *LineData) (string, error) {
var kakaoID string
// var eid string
var err error
var warn string
url, _ := url.Parse(link)
switch url.Host {
// Kakao web link.
case "e.kakao.com":
kakaoID = path.Base(url.Path)
// Kakao mobile app share link.
case "emoticon.kakao.com":
_, kakaoID, err = fetchKakaoDetailsFromShareLink(link)
if err != nil {
return warn, err
}
// unknown host
default:
return warn, errors.New("unknown kakao link type")
}
var kakaoJson KakaoJson
err = fetchKakaoMetadata(&kakaoJson, kakaoID)
if err != nil {
log.Debugln("Failed fetchKakaoMetadata:", err)
return warn, err
}
log.Debugln("Parsed kakao link:", link)
log.Debugln(kakaoJson.Result)
// if url.Host == "emoticon.kakao.com" {
// ld.DLink = fmt.Sprintf("http://item.kakaocdn.net/dw/%s.file_pack.zip", eid)
// } else {
ld.DLinks = kakaoJson.Result.ThumbnailUrls
// warn = WARN_KAKAO_PREFER_SHARE_LINK
// }
ld.Title = kakaoJson.Result.Title
ld.Id = kakaoJson.Result.TitleUrl
ld.Link = link
ld.Amount = len(ld.DLinks)
ld.Category = KAKAO_EMOTICON
return warn, nil
}
func fetchKakaoMetadata(kakaoJson *KakaoJson, kakaoID string) error {
apiUrl := "https://e.kakao.com/api/v1/items/t/" + kakaoID
page, err := httpGet(apiUrl)
if err != nil {
return err
}
err = json.Unmarshal([]byte(page), &kakaoJson)
if err != nil {
log.Errorln("Failed json parsing kakao link!", err)
return err
}
log.Debugln("fetchKakaoMetadata: api link metadata fetched:", apiUrl)
return nil
}
// Download and convert(if needed) stickers to work directory.
// *ld will be modified and loaded with local sticker information.
func prepareKakaoStickers(ctx context.Context, ld *LineData, workDir string, needConvert bool) error {
// If no dLink, continue importing static ones.
if ld.DLink != "" {
return prepareKakaoZipStickers(ctx, ld, workDir, needConvert)
}
os.MkdirAll(workDir, 0755)
//Initialize Files with wg added.
//This is intended for async operation.
//When user reached commitSticker state, sticker will be waited one by one.
for range ld.DLinks {
lf := &LineFile{}
lf.Wg.Add(1)
ld.Files = append(ld.Files, lf)
}
//Download stickers one by one.
go func() {
for i, l := range ld.DLinks {
select {
case <-ctx.Done():
log.Warn("prepareKakaoStickers received ctxDone!")
return
default:
}
f := filepath.Join(workDir, path.Base(l)+".png")
err := httpDownload(l, f)
if err != nil {
ld.Files[i].CError = err
}
cf, _ := IMToWebpTGStatic(f, false)
ld.Files[i].OriginalFile = f
ld.Files[i].ConvertedFile = cf
ld.Files[i].Wg.Done()
log.Debug("Done process one kakao emoticon")
log.Debugf("f:%s, cf:%s", f, cf)
}
log.Debug("Done process ALL kakao emoticons")
}()
return nil
}
func prepareKakaoZipStickers(ctx context.Context, ld *LineData, workDir string, needConvert bool) error {
zipPath := filepath.Join(workDir, "kakao.zip")
os.MkdirAll(workDir, 0755)
log.Debugln("prepareKakaoZipStickers: downloading zip:", ld.DLink)
err := fDownload(ld.DLink, zipPath)
if err != nil {
return err
}
kakaoFiles := kakaoZipExtract(zipPath, ld)
if len(kakaoFiles) == 0 {
return errors.New("no kakao image in zip")
}
if filepath.Ext(kakaoFiles[0]) != ".png" {
ld.IsAnimated = true
}
for _, wf := range kakaoFiles {
lf := &LineFile{
OriginalFile: wf,
}
if needConvert {
lf.Wg.Add(1)
}
ld.Files = append(ld.Files, lf)
}
ld.Amount = len(kakaoFiles)
if needConvert {
go convertSToTGFormat(ctx, ld)
}
log.Debug("Done preparing kakao files:")
log.Debugln(ld)
return nil
}
// Extract and decrypt kakao zip.
func kakaoZipExtract(f string, ld *LineData) []string {
var files []string
workDir := fExtract(f)
if workDir == "" {
return nil
}
log.Debugln("scanning workdir:", workDir)
files = LsFiles(workDir, []string{}, []string{})
for _, f := range files {
//PNG is not encrypted.
if filepath.Ext(f) != ".png" {
//This script decrypts the file in-place.
exec.Command("msb_kakao_decrypt.py", f).Run()
}
}
return files
}
// Return: kakao eid(code), kakao id, error
func fetchKakaoDetailsFromShareLink(link string) (string, string, error) {
log.Debugln("fetchKakaoDetailsFromShareLink: Link is:", link)
res, err := httpGetAndroidUA(link)
if err != nil {
log.Errorln("fetchKakaoDetailsFromShareLink: failed httpGetAndroidUA!", err)
return "", "", err
}
doc, err := goquery.NewDocumentFromReader(strings.NewReader(res))
if err != nil {
log.Errorln("fetchKakaoDetailsFromShareLink failed gq parsing line link!", err)
return "", "", err
}
//This eid seemed to be fake.
//There will be no fix soon.
//In the future we might use other package to complete
//kakao download.
eid := ""
doc.Find("a").Each(func(i int, s *goquery.Selection) {
value, _ := s.Attr("id")
if value == "app_scheme_link" {
eid, _ = s.Attr("data-i")
}
})
log.Debugln("kakao eid is:", eid)
redirLink, _, err := httpGetWithRedirLink(link)
if err != nil {
return "", "", err
}
kakaoID := path.Base(redirLink)
return eid, kakaoID, nil
}
================================================
FILE: pkg/msbimport/import_line.go
================================================
package msbimport
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"github.com/PuerkitoBio/goquery"
log "github.com/sirupsen/logrus"
)
func parseLineLink(link string, ld *LineData) (string, error) {
var warn string
page, err := httpGet(link)
if err != nil {
return warn, err
}
doc, err := goquery.NewDocumentFromReader(strings.NewReader(page))
if err != nil {
log.Errorln("Failed gq parsing line link!", err)
return warn, err
}
var lineJson LineJson
err = parseLineDetails(doc, &lineJson)
if err != nil {
log.Errorln("parseLineLink: ", err)
return warn, err
}
t := lineJson.Name
i := lineJson.Sku
u := lineJson.Url
ls := fetchLineI18nLinks(doc)
a := false
e := false
c := ""
d := "https://stickershop.line-scdn.net/stickershop/v1/product/" + i + "/iphone/"
if strings.Contains(u, "stickershop") || strings.Contains(u, "officialaccount/event/sticker") {
if strings.Contains(page, "MdIcoPlay_b") || strings.Contains(page, "MdIcoAni_b") {
c = LINE_STICKER_ANIMATION
d += "stickerpack@2x.zip"
a = true
} else if strings.Contains(page, "MdIcoMessageSticker_b") {
d = u
c = LINE_STICKER_MESSAGE
} else if strings.Contains(page, "MdIcoNameSticker_b") {
d += "sticker_name_base@2x.zip"
c = LINE_STICKER_NAME
} else if strings.Contains(page, "MdIcoFlash_b") || strings.Contains(page, "MdIcoFlashAni_b") {
c = LINE_STICKER_POPUP
d += "stickerpack@2x.zip"
a = true
} else if strings.Contains(page, "MdIcoEffectSoundSticker_b") || strings.Contains(page, "MdIcoEffectSticker_b") {
c = LINE_STICKER_POPUP_EFFECT
d += "stickerpack@2x.zip"
a = true
} else {
c = LINE_STICKER_STATIC
// According to collected logs, LINE ID befre exact 775 have special PNG encodings,
// which are not parsable with libpng.
// You will get -> CgBI: unhandled critical chunk <- from IM.
// Workaround is to take the lower resolution "android" ones.
if id, _ := strconv.Atoi(i); id < 775 && id != 0 {
d = "https://stickershop.line-scdn.net/stickershop/v1/product/" + i + "/android/" +
"stickers.zip"
} else {
d += "stickers@2x.zip"
}
}
} else if strings.Contains(u, "emojishop") {
if strings.Contains(page, "MdIcoPlay_b") {
c = LINE_EMOJI_ANIMATION
d = "https://stickershop.line-scdn.net/sticonshop/v1/sticon/" + i + "/iphone/package_animation.zip"
a = true
e = true
} else {
c = LINE_EMOJI_STATIC
e = true
d = "https://stickershop.line-scdn.net/sticonshop/v1/sticon/" + i + "/iphone/package.zip"
}
} else {
return warn, errors.New("unknown line store category")
}
if ld == nil {
return warn, nil
}
ld.Link = u
ld.I18nLinks = ls
ld.Category = c
ld.DLink = d
ld.Id = i
ld.Title = t
ld.IsAnimated = a
ld.IsEmoji = e
log.Debugln("line data parsed:", ld)
ld.TitleWg.Add(1)
go fetchLineI18nTitles(ld)
return warn, nil
}
func fetchLineI18nLinks(doc *goquery.Document) []string {
var i18nLinks []string
doc.Find("link").Each(func(i int, s *goquery.Selection) {
hreflang, exist := s.Attr("hreflang")
if !exist {
return
}
href, exist2 := s.Attr("href")
if !exist2 {
return
}
switch hreflang {
case "zh-Hant":
fallthrough
case "ja":
fallthrough
case "en":
i18nLinks = append(i18nLinks, href)
}
})
log.Debugln("Fetched LINE I18n Links: ", i18nLinks)
return i18nLinks
}
func fetchLineI18nTitles(ld *LineData) {
defer ld.TitleWg.Done()
log.Debugln("Fetching LINE i18n titles...")
var i18nTitles []string
for _, l := range ld.I18nLinks {
page, err := httpGet(l)
if err != nil {
continue
}
lineJson := &LineJson{}
doc, err := goquery.NewDocumentFromReader(strings.NewReader(page))
if err != nil {
continue
}
parseLineDetails(doc, lineJson)
for _, t := range i18nTitles {
// if title duplicates, skip loop
if t == lineJson.Name {
goto CONTINUE
}
}
i18nTitles = append(i18nTitles, lineJson.Name)
CONTINUE:
continue
}
ld.I18nTitles = i18nTitles
log.Debugln("Fetched I18N titles are:")
log.Debugln(ld.I18nTitles)
}
// This function goes after parseLineLink
// Receives a gq document of LINE Store page and parse the details to *LineJson.
func parseLineDetails(doc *goquery.Document, lj *LineJson) error {
// For typical line store page, the first script is sticker's metadata in JSON.
// Parse to json, if OK, return nil here.
err := json.Unmarshal([]byte(doc.Find("script").First().Text()), lj)
if err == nil {
return nil
}
// Some new line store page does not have a json metadata
React App