Showing preview only (372K chars total). Download the full file or copy to clipboard to get everything.
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. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU 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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU 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 <https://www.gnu.org/licenses/>.
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:
<program> Copyright (C) <year> <name of author>
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
<https://www.gnu.org/licenses/>.
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
<https://www.gnu.org/licenses/why-not-lgpl.html>.
================================================
FILE: README.md
================================================
# [@moe_sticker_bot](https://t.me/moe_sticker_bot)
[](https://pkg.go.dev/github.com/star-39/moe-sticker-bot)   
[<img width="500" src="https://user-images.githubusercontent.com/75669297/222379608-1359ac0f-18ed-4a25-a91e-32974994d27b.png">](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)
<img width="487" alt="スクリーンショット 2023-02-27 午後7 29 35" src="https://user-images.githubusercontent.com/75669297/221539624-c0cc32a9-477c-425f-8e98-6566326385b4.png">
<img width="500" alt="スクリーンショット 2023-02-27 午後7 24 14" src="https://user-images.githubusercontent.com/75669297/221538927-526a878a-5d86-4b45-ab9a-d324743e3b91.png">
<img width="500" alt="スクリーンショット 2023-02-27 午後7 37 17" src="https://user-images.githubusercontent.com/75669297/221541547-4618c9ef-9be3-4d50-b7da-fdc5b25e64b8.png">
<!-- <img width="500" alt="スクリーンショット 2023-02-27 午後7 21 22" src="https://user-images.githubusercontent.com/75669297/221538953-6c69dc08-5cb1-4f07-a9ce-43bacb9f1566.png"> -->
<!-- 
<img width="511" alt="スクリーンショット 2022-03-24 19 58 09" src="https://user-images.githubusercontent.com/75669297/159902095-fefbcbbf-1c5e-4c3e-9e55-eb28b48567e0.png"> -->
<!--<img width="500" alt="スクリーンショット 2022-05-11 19 33 27" src="https://user-images.githubusercontent.com/75669297/167830628-1dfc9941-4b3c-408d-bf64-1ef814e3efe8.png"> <img width="500" alt="スクリーンショット 2022-05-11 19 51 46" src="https://user-images.githubusercontent.com/75669297/167833015-806b4f11-ecd9-4f10-9b9c-ecb7a20f8f97.png">-->
<!--<img width="500" alt="スクリーンショット 2022-05-11 19 58 52" src="https://user-images.githubusercontent.com/75669297/167834522-f1e988e8-bd24-44b1-a90c-f69791a9a623.png">
<img width="500" alt="スクリーンショット 2023-02-11 午前1 53 55" src="https://user-images.githubusercontent.com/75669297/218149914-65db79c0-c3f9-44ed-8043-1673eba41bc0.png">
-->
<!--
<img width="500" alt="スクリーンショット 2023-02-11 午前2 15 56" src="https://user-images.githubusercontent.com/75669297/218154599-c0af7aa5-e8ff-4f6d-9110-9b7175fbe585.png">
<img width="517" alt="スクリーンショット 2022-12-12 午後2 31 32" src="https://user-images.githubusercontent.com/75669297/206968834-d86c69d5-7e1d-4e36-9370-a66addc0c4fa.png">
-->
<!--
<img width="535" alt="スクリーンショット 2022-12-12 午後2 26 46" src="https://user-images.githubusercontent.com/75669297/206968863-1bb7e5cd-0c43-4573-8292-3e3e629f39bf.png">
<img width="562" alt="スクリーンショット 2023-02-11 午前2 21 40" src="https://user-images.githubusercontent.com/75669297/218155866-912739bc-b954-4ca2-97c1-d99e43f02a89.png">
<!--<img width="517" alt="スクリーンショット 2022-12-12 午後2 47 22" src="https://user-images.githubusercontent.com/75669297/206969650-cff19478-898a-4344-a73a-80469184053c.png">
-->
<img width="394" alt="スクリーンショット 2022-12-12 午後2 27 10" src="https://user-images.githubusercontent.com/75669297/206968889-1fe25c05-6071-422b-9e1b-549d56f5d351.png">
<img width="500" alt="スクリーンショット 2023-02-11 午前2 24 37" src="https://user-images.githubusercontent.com/75669297/218156358-0145264f-ab11-4010-bfcd-2e38621d7381.png">
<img width="300" src="https://user-images.githubusercontent.com/75669297/218153727-5fb1d3e0-3770-4dc8-a2b5-3e0ecd89a003.png"/> <img width="300" src="https://user-images.githubusercontent.com/75669297/221529085-2581bcca-fe49-46b0-8123-5614e90a838c.png"/>
## 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貼圖匯入的問題.
<details>
<summary>Detailed 2.0 Changelogs 詳細的2.0變更列表</summary>
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.
</details>
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.
<details>
<summary>Old changelogs</summary>
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.
</details>
<!--
## Technical Details

-->
## Special Thanks:
[<img width=200 src="https://idcs-cb5322c0a68345bb83637843d27aa437.identity.oraclecloud.com/ui/v1/public/common/asset/defaultBranding/oracle-desktop-logo.gif">](https://www.oracle.com/cloud/) for free 4CPU AArch64 Cloud Instance.
<a href="http://t.me/StickerGroup">貼圖群 - Sticker Group Taiwan</a> 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 <a href="https://github.com/star-39/moe-sticker-bot">moe_sticker_bot</a>! Please:
• Send <b>LINE/Kakao sticker share link</b> to import or download.
• Send <b>Telegram sticker/link/GIF</b> to download or export to WhatsApp.
• Send <b>keywords</b> to search sticker sets.
• Tap to <b>/create</b> or <b>/manage</b> sticker set and CustomEmoji.
• Tap to check all available <b>/command_list</b>.
你好! 歡迎使用<a href="https://github.com/star-39/moe-sticker-bot">萌萌貼圖BOT</a>! 請:
• 傳送<b>LINE/kakao貼圖包的分享連結</b>來匯入或下載.
• 傳送<b>Telegram貼圖/連結/GIF</b>來下載.
• 傳送<b>關鍵字</b>來搜尋貼圖包.
• 傳送 <b>/create</b> 或 <b>/manage</b> 來創建或管理貼圖包和表情貼。
• 傳送 <b>/command_list</b> 檢視所有可用指令.
`
return c.Send(message, tele.ModeHTML, tele.NoPreview)
}
func sendCommandList(c tele.Context) error {
message := `
<b>/import</b> <b>/search</b> LINE/Kakao stickers.<code>
匯入或搜尋LINE/Kaka貼圖包.</code>
<b>/download</b> <b>/create</b> <b>/manage</b> Telegram stickers.<code>
下載、創建、管理Telegram貼圖包.</code>
<b>/faq /about /changelog /privacy</b><code>
常見問題/關於/更新紀錄/私隱</code>
`
return c.Send(message, tele.ModeHTML, tele.NoPreview)
}
func sendAboutMessage(c tele.Context) {
c.Send(fmt.Sprintf(`
<b>Please star for this project on Github if you like this bot!
如果您喜歡這個bot, 歡迎在Github給本專案標Star喔!
https://github.com/star-39/moe-sticker-bot</b>
Thank you @StickerGroup for feedbacks and advices!
<code>
This free(as in freedom) software is released under the GPLv3 License.
Comes with ABSOLUTELY NO WARRANTY! All rights reserved.
本BOT為免費提供的自由軟體, 您可以自由使用/分發, 惟無任何保用(warranty)!
本軟體授權於通用公眾授權條款(GPL)v3, 保留所有權利.
</code><b>
Please send /start to start using
請傳送 /start 來開始
</b><code>
Version:版本: %s
</code>
`, BOT_VERSION), tele.ModeHTML)
}
func sendFAQ(c tele.Context) {
c.Send(fmt.Sprintf(`
<b>Please hit Star for this project on Github if you like this bot!
如果您喜歡這個bot, 歡迎在Github給本專案標Star喔!
https://github.com/star-39/moe-sticker-bot</b>
------------------------------------
<b>Q: I got stucked! I can't quit from command!
我卡住了! 我沒辦法從指令中退出!</b>
A: Please send /quit to interrupt.
請傳送 /quit 來中斷.
<b>Q: Why ID has suffix: _by_%s ?
為甚麼ID的末尾有: _by_%s ?</b>
A: It's forced by Telegram, bot created sticker set must have its name in ID suffix.
因為這個是Telegram的強制要求, 由bot創造的貼圖ID末尾必須有bot名字.
<b>Q: Who owns the sticker sets the bot created?
BOT創造的貼圖包由誰所有?</b>
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(`
<b>Privacy Notice:</b>
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.
<b>私隱聲明:</b>
本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 <a href="https://github.com/star-39/msb_app">Msb App</a> due to their restrictions, then press "Continue export".
匯出到WhatsApp需要手機上安裝<a href="https://github.com/star-39/msb_app">Msb App</a>, 然後按下"繼續匯出".
Download:下載:
<b>iPhone:</b> AppStore(N/A.暫無), <a href="https://github.com/star-39/msb_app/releases/latest/download/msb_app.ipa">IPA</a>
<b>Android:</b> GooglePlay(N/A.暫無), <a href="https://github.com/star-39/msb_app/releases/latest/download/msb_app.apk">APK</a>
`, 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<code>" + title + "</code>"
}
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: 例如:
<code>My_favSticker21</code>
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: 例如:
<code>https://store.line.me/stickershop/product/7673/ja</code>
<code>https://e.kakao.com/t/pretty-all-friends</code>
<code>https://emoticon.kakao.com/items/lV6K2fWmU7CpXlHcP9-ysQJx9rg=?referer=share_link</code>
`, 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(`<a href="%s">%s</a>`, "https://t.me/addstickers/"+l.Tg_id, l.Tg_title))
} else {
// append to top
entries = append([]string{fmt.Sprintf(`★ <a href="%s">%s</a>`, "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(`<a href="%s">%s</a>`, "https://t.me/addstickers/"+l.Tg_id, l.Tg_title))
} else {
// append to top
entries = append([]string{fmt.Sprintf(`★ <a href="%s">%s</a>`, "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("<b>Fatal error encounterd. Please try again. /start\n"+
"發生嚴重錯誤. 請您從頭再試一次. /start </b>\n\n"+
"You can report this error to https://github.com/star-39/moe-sticker-bot/issues\n\n"+
"<code>"+errMsg+"</code>", tele.ModeHTML, tele.NoPreview)
}
func sendExecEmojiAssignFinished(c tele.Context) error {
ud := users.data[c.Sender().ID]
msg := fmt.Sprintf(`
LINE Cat: <code>%s</code>
LINE ID: <code>%s</code>
TG ID: <code>%s</code>
TG Title: <a href="%s">%s</a>
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: <code>%s</code>
LINE ID: <code>%s</code>
TG ID: <code>%s</code>
TG TYPE: <code>%s</code>
TG Title: <a href="%s">%s</a>
<b>Progress / 進展</b>
<code>%s</code>
`, 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, "<code>")]
prog := ""
if progressText != "" {
prog = progressText
goto SEND
}
cur = cur + 1
if cur == 1 {
prog = fmt.Sprintf("<code>[=> ]\n %d of %d</code>", cur, total)
} else if cur == int(float64(0.25)*float64(total)) {
prog = fmt.Sprintf("<code>[====> ]\n %d of %d</code>", cur, total)
} else if cur == int(float64(0.5)*float64(total)) {
prog = fmt.Sprintf("<code>[=========> ]\n %d of %d</code>", cur, total)
} else if cur == int(float64(0.75)*float64(total)) {
prog = fmt.Sprintf("<code>[==============> ]\n %d of %d</code>", cur, total)
} else if cur == total {
prog = fmt.Sprintf("<code>[====================]\n %d of %d</code>", 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(`<a href="https://t.me/addstickers/%s">%s</a>`, 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: <code>%s</code>
Title: <a href="https://t.me/addstickers/%s">%s</a>
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"+
"<code>https://store.line.me/stickershop/product/7673/ja</code>\n"+
"<code>https://e.kakao.com/t/pretty-all-friends</code>", 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:例如: <code>https://emoticon.kakao.com/items/lV6K2fWmU7CpXlHcP9-ysQJx9rg=?referer=share_link</code>
`
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.202102170
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
SYMBOL INDEX (316 symbols across 33 files)
FILE: cmd/moe-sticker-bot/main.go
function main (line 16) | func main() {
function parseCmdLine (line 21) | func parseCmdLine() core.ConfigTemplate {
FILE: cmd/msbimport/main.go
function main (line 14) | func main() {
FILE: core/admin.go
function cmdSitRep (line 7) | func cmdSitRep(c tele.Context) error {
function cmdGetFID (line 17) | func cmdGetFID(c tele.Context) error {
FILE: core/commands.go
function cmdCreate (line 10) | func cmdCreate(c tele.Context) error {
function cmdManage (line 15) | func cmdManage(c tele.Context) error {
function cmdImport (line 26) | func cmdImport(c tele.Context) error {
function cmdDownload (line 32) | func cmdDownload(c tele.Context) error {
function cmdAbout (line 38) | func cmdAbout(c tele.Context) error {
function cmdFAQ (line 43) | func cmdFAQ(c tele.Context) error {
function cmdPrivacy (line 48) | func cmdPrivacy(c tele.Context) error {
function cmdChangelog (line 52) | func cmdChangelog(c tele.Context) error {
function cmdStart (line 56) | func cmdStart(c tele.Context) error {
function cmdCommandList (line 60) | func cmdCommandList(c tele.Context) error {
function cmdSearch (line 64) | func cmdSearch(c tele.Context) error {
function cmdGroupSearch (line 72) | func cmdGroupSearch(c tele.Context) error {
function cmdQuit (line 85) | func cmdQuit(c tele.Context) error {
FILE: core/config.go
type ConfigTemplate (line 5) | type ConfigTemplate struct
FILE: core/database.go
constant DB_VER (line 62) | DB_VER = "2"
function initDB (line 64) | func initDB(dbname string) error {
function verifyDB (line 105) | func verifyDB(dsn *mysql.Config, dbname string) error {
function checkUpgradeDatabase (line 125) | func checkUpgradeDatabase(queriedDbVer string) {
function createMariadb (line 133) | func createMariadb(dsn *mysql.Config, dbname string) error {
function insertLineS (line 151) | func insertLineS(lineID string, lineLink string, tgID string, tgTitle st...
function insertUserS (line 169) | func insertUserS(uid int64, tgID string, tgTitle string, timestamp int64) {
function queryLineS (line 188) | func queryLineS(id string) []LineStickerQ {
function queryUserS (line 224) | func queryUserS(uid int64) []UserStickerQ {
function matchUserS (line 260) | func matchUserS(uid int64, id string) bool {
function deleteUserS (line 273) | func deleteUserS(tgID string) {
function deleteLineS (line 285) | func deleteLineS(tgID string) {
function updateLineSAE (line 297) | func updateLineSAE(ae bool, tgID string) error {
function searchLineS (line 305) | func searchLineS(keywords []string) []LineStickerQ {
function curateDatabase (line 346) | func curateDatabase() error {
FILE: core/define.go
constant CB_DN_WHOLE (line 28) | CB_DN_WHOLE = "dall"
constant CB_DN_SINGLE (line 29) | CB_DN_SINGLE = "dsingle"
constant CB_OK_IMPORT (line 30) | CB_OK_IMPORT = "yesimport"
constant CB_OK_IMPORT_EMOJI (line 31) | CB_OK_IMPORT_EMOJI = "yesimportemoji"
constant CB_OK_DN (line 32) | CB_OK_DN = "yesd"
constant CB_BYE (line 33) | CB_BYE = "bye"
constant CB_MANAGE (line 34) | CB_MANAGE = "manage"
constant CB_DONE_ADDING (line 35) | CB_DONE_ADDING = "done"
constant CB_YES (line 36) | CB_YES = "yes"
constant CB_NO (line 37) | CB_NO = "no"
constant CB_DEFAULT_TITLE (line 38) | CB_DEFAULT_TITLE = "titledefault"
constant CB_EXPORT_WA (line 39) | CB_EXPORT_WA = "exportwa"
constant CB_ADD_STICKER (line 40) | CB_ADD_STICKER = "adds"
constant CB_DELETE_STICKER (line 41) | CB_DELETE_STICKER = "dels"
constant CB_DELETE_STICKER_SET (line 42) | CB_DELETE_STICKER_SET = "delss"
constant CB_CHANGE_TITLE (line 43) | CB_CHANGE_TITLE = "changetitle"
constant CB_REGULAR_STICKER (line 44) | CB_REGULAR_STICKER = "regular"
constant CB_CUSTOM_EMOJI (line 45) | CB_CUSTOM_EMOJI = "customemoji"
constant ST_WAIT_WEBAPP (line 47) | ST_WAIT_WEBAPP = "waitWebApp"
constant ST_PROCESSING (line 48) | ST_PROCESSING = "process"
constant FID_KAKAO_SHARE_LINK (line 50) | FID_KAKAO_SHARE_LINK = "AgACAgEAAxkBAAEjezVj3_YXwaQ8DM-107IzlLSaXyG6yAAC...
constant LINK_TG (line 52) | LINK_TG = "t.me"
constant LINK_LINE (line 53) | LINK_LINE = "line.me"
constant LINK_KAKAO (line 54) | LINK_KAKAO = "kakao.com"
constant LINK_IMPORT (line 55) | LINK_IMPORT = "IMPORT"
type LineStickerQ (line 59) | type LineStickerQ struct
type UserStickerQ (line 68) | type UserStickerQ struct
type WebAppUser (line 75) | type WebAppUser struct
type UserData (line 87) | type UserData struct
method udSetState (line 128) | func (ud *UserData) udSetState(state string) {
type Users (line 113) | type Users struct
type StickerMoveObject (line 120) | type StickerMoveObject struct
type StickerFile (line 135) | type StickerFile struct
type StickerData (line 155) | type StickerData struct
type StickerDownloadObject (line 178) | type StickerDownloadObject struct
FILE: core/init.go
function Init (line 17) | func Init(conf ConfigTemplate) {
function Recover (line 70) | func Recover(onError ...func(error)) tele.MiddlewareFunc {
function endSession (line 98) | func endSession(c tele.Context) {
function terminateSession (line 103) | func terminateSession(c tele.Context) {
function endManageSession (line 108) | func endManageSession(c tele.Context) {
function onError (line 120) | func onError(err error, c tele.Context) {
function initBot (line 137) | func initBot(conf ConfigTemplate) *tele.Bot {
function initWorkspace (line 158) | func initWorkspace(b *tele.Bot) {
function initGoCron (line 183) | func initGoCron() {
function initLogrus (line 194) | func initLogrus() {
FILE: core/message.go
function sendStartMessage (line 17) | func sendStartMessage(c tele.Context) error {
function sendCommandList (line 36) | func sendCommandList(c tele.Context) error {
function sendAboutMessage (line 49) | func sendAboutMessage(c tele.Context) {
function sendFAQ (line 69) | func sendFAQ(c tele.Context) {
function sendChangelog (line 92) | func sendChangelog(c tele.Context) error {
function sendPrivacy (line 156) | func sendPrivacy(c tele.Context) error {
function sendAskEmoji (line 190) | func sendAskEmoji(c tele.Context) error {
function sendConfirmExportToWA (line 206) | func sendConfirmExportToWA(c tele.Context, sn string, hex string) error {
function genSDnMnEInline (line 225) | func genSDnMnEInline(canManage bool, isTGS bool, sn string) *tele.ReplyM...
function sendAskSDownloadChoice (line 244) | func sendAskSDownloadChoice(c tele.Context, s *tele.Sticker) error {
function sendAskSChoice (line 252) | func sendAskSChoice(c tele.Context, sn string) error {
function sendAskTGLinkChoice (line 260) | func sendAskTGLinkChoice(c tele.Context) error {
function sendAskWantSDown (line 271) | func sendAskWantSDown(c tele.Context) error {
function sendAskWantImportOrDownload (line 282) | func sendAskWantImportOrDownload(c tele.Context, avalAsEmoji bool) error {
function sendAskWhatToDownload (line 305) | func sendAskWhatToDownload(c tele.Context) error {
function sendAskTitle_Import (line 310) | func sendAskTitle_Import(c tele.Context) error {
function sendAskTitle (line 341) | func sendAskTitle(c tele.Context) error {
function sendAskID (line 346) | func sendAskID(c tele.Context) error {
function sendAskImportLink (line 362) | func sendAskImportLink(c tele.Context) error {
function sendNotifySExist (line 373) | func sendNotifySExist(c tele.Context, lineID string) bool {
function sendSearchResult (line 398) | func sendSearchResult(entriesWant int, lines []LineStickerQ, c tele.Cont...
function sendAskStickerFile (line 434) | func sendAskStickerFile(c tele.Context) error {
function sendInStateWarning (line 443) | func sendInStateWarning(c tele.Context) error {
function sendNoSessionWarning (line 457) | func sendNoSessionWarning(c tele.Context) error {
function sendAskSTypeToCreate (line 461) | func sendAskSTypeToCreate(c tele.Context) error {
function sendAskEmojiAssign (line 471) | func sendAskEmojiAssign(c tele.Context) error {
function sendFatalError (line 516) | func sendFatalError(err error, c tele.Context) {
function sendExecEmojiAssignFinished (line 536) | func sendExecEmojiAssignFinished(c tele.Context) error {
function sendProcessStarted (line 558) | func sendProcessStarted(ud *UserData, c tele.Context, optMsg string) (st...
function editProgressMsg (line 586) | func editProgressMsg(cur int, total int, progressText string, originalTe...
function sendAskSToManage (line 620) | func sendAskSToManage(c tele.Context) error {
function sendUserOwnedS (line 627) | func sendUserOwnedS(c tele.Context) error {
function sendAskEditChoice (line 662) | func sendAskEditChoice(c tele.Context) error {
function sendAskSDel (line 701) | func sendAskSDel(c tele.Context) error {
function sendConfirmDelset (line 706) | func sendConfirmDelset(c tele.Context) error {
function sendSFromSS (line 716) | func sendSFromSS(c tele.Context, ssid string, reply *tele.Message) error {
function sendFLWarning (line 726) | func sendFLWarning(c tele.Context) error {
function sendTooManyFloodLimits (line 737) | func sendTooManyFloodLimits(c tele.Context) error {
function sendNoCbWarn (line 742) | func sendNoCbWarn(c tele.Context) error {
function sendBadIDWarn (line 746) | func sendBadIDWarn(c tele.Context) error {
function sendIDOccupiedWarn (line 754) | func sendIDOccupiedWarn(c tele.Context) error {
function sendBadImportLinkWarn (line 758) | func sendBadImportLinkWarn(c tele.Context) error {
function sendNoSToManage (line 766) | func sendNoSToManage(c tele.Context) error {
function sendPromptStopAdding (line 771) | func sendPromptStopAdding(c tele.Context) error {
function replySFileOK (line 779) | func replySFileOK(c tele.Context, count int) error {
function sendSEditOK (line 788) | func sendSEditOK(c tele.Context) error {
function sendStickerSetFullWarning (line 794) | func sendStickerSetFullWarning(c tele.Context) error {
function sendAskSearchKeyword (line 804) | func sendAskSearchKeyword(c tele.Context) error {
function sendSearchNoResult (line 808) | func sendSearchNoResult(c tele.Context) error {
function sendNotifyNoSessionSearch (line 816) | func sendNotifyNoSessionSearch(c tele.Context) error {
function sendUnsupportedCommandForGroup (line 821) | func sendUnsupportedCommandForGroup(c tele.Context) error {
function sendBadSearchKeyword (line 826) | func sendBadSearchKeyword(c tele.Context) error {
function sendPreferKakaoShareLinkWarning (line 837) | func sendPreferKakaoShareLinkWarning(c tele.Context) error {
function sendUseCommandToImport (line 859) | func sendUseCommandToImport(c tele.Context) error {
function sendOneStickerFailedToAdd (line 864) | func sendOneStickerFailedToAdd(c tele.Context, pos int, err error) error {
function sendBadSNWarn (line 873) | func sendBadSNWarn(c tele.Context) error {
function sendSSTitleChanged (line 877) | func sendSSTitleChanged(c tele.Context) error {
function sendSSTitleFailedToChanged (line 883) | func sendSSTitleFailedToChanged(c tele.Context) error {
function sendProcessingStickers (line 897) | func sendProcessingStickers(c tele.Context) error {
FILE: core/os_util.go
function purgeOutdatedStorageData (line 11) | func purgeOutdatedStorageData() {
FILE: core/states.go
function handleMessage (line 16) | func handleMessage(c tele.Context) error {
function handleNoSession (line 82) | func handleNoSession(c tele.Context) error {
function confirmImport (line 172) | func confirmImport(c tele.Context, wantEmoji bool) error {
function trySearchKeyword (line 212) | func trySearchKeyword(c tele.Context) bool {
function stateProcessing (line 225) | func stateProcessing(c tele.Context) error {
function statePrepareSManage (line 234) | func statePrepareSManage(c tele.Context) error {
function waitCbEditChoice (line 269) | func waitCbEditChoice(c tele.Context) error {
function waitSDel (line 296) | func waitSDel(c tele.Context) error {
function waitCbDelset (line 323) | func waitCbDelset(c tele.Context) error {
function waitSType (line 348) | func waitSType(c tele.Context) error {
function waitSFile (line 367) | func waitSFile(c tele.Context) error {
function waitSTitle (line 399) | func waitSTitle(c tele.Context) error {
function waitSID (line 449) | func waitSID(c tele.Context) error {
function waitEmojiChoice (line 473) | func waitEmojiChoice(c tele.Context) error {
function waitSEmojiAssign (line 509) | func waitSEmojiAssign(c tele.Context) error {
function waitSearchKeyword (line 537) | func waitSearchKeyword(c tele.Context) error {
FILE: core/sticker.go
function submitStickerSetAuto (line 21) | func submitStickerSetAuto(createSet bool, c tele.Context) error {
function submitStickerManual (line 137) | func submitStickerManual(createSet bool, pos int, emojis []string, keywo...
function finalizeSubmitStickerManual (line 190) | func finalizeSubmitStickerManual(c tele.Context, createSet bool, ud *Use...
function createStickerSet (line 205) | func createStickerSet(safeMode bool, sf *StickerFile, c tele.Context, na...
function createStickerSetBatch (line 261) | func createStickerSetBatch(sfs []*StickerFile, c tele.Context, name stri...
function commitSingleticker (line 295) | func commitSingleticker(pos int, flCount *int, safeMode bool, sf *Sticke...
function editStickerEmoji (line 401) | func editStickerEmoji(newEmojis []string, fid string, ud *UserData) error {
function appendMedia (line 407) | func appendMedia(c tele.Context) error {
function guessIsArchive (line 493) | func guessIsArchive(f string) bool {
function verifyFloodedStickerSet (line 504) | func verifyFloodedStickerSet(c tele.Context, fc int, ec int, desiredAmou...
FILE: core/sticker_download.go
function downloadStickersAndSend (line 18) | func downloadStickersAndSend(s *tele.Sticker, setID string, c tele.Conte...
function downloadGifToZip (line 123) | func downloadGifToZip(c tele.Context) error {
function downloadLineSToZip (line 144) | func downloadLineSToZip(c tele.Context, ud *UserData) error {
FILE: core/userdata.go
function cleanUserDataAndDir (line 14) | func cleanUserDataAndDir(uid int64) bool {
function cleanUserData (line 30) | func cleanUserData(uid int64) bool {
function initUserData (line 45) | func initUserData(c tele.Context, command string, state string) *UserData {
function getState (line 69) | func getState(c tele.Context) (string, string) {
function checkState (line 78) | func checkState(next tele.HandlerFunc) tele.HandlerFunc {
function setState (line 106) | func setState(c tele.Context, state string) {
FILE: core/util.go
function checkTitle (line 29) | func checkTitle(t string) bool {
function checkID (line 37) | func checkID(s string) bool {
function secHex (line 59) | func secHex(n int) string {
function findLink (line 74) | func findLink(s string) string {
function findLinkWithType (line 79) | func findLinkWithType(s string) (string, string) {
function findEmojis (line 101) | func findEmojis(s string) string {
function findEmojiList (line 109) | func findEmojiList(s string) []string {
function stripEmoji (line 119) | func stripEmoji(s string) string {
function sanitizeCallback (line 127) | func sanitizeCallback(next tele.HandlerFunc) tele.HandlerFunc {
function autoRespond (line 136) | func autoRespond(next tele.HandlerFunc) tele.HandlerFunc {
function escapeTagMark (line 145) | func escapeTagMark(s string) string {
function getSIDFromMessage (line 151) | func getSIDFromMessage(m *tele.Message) string {
function retrieveSSDetails (line 160) | func retrieveSSDetails(c tele.Context, id string, sd *StickerData) error {
function GetUd (line 176) | func GetUd(uidS string) (*UserData, error) {
function sliceMove (line 189) | func sliceMove[T any](oldIndex int, newIndex int, slice []T) []T {
function chunkSlice (line 212) | func chunkSlice(slice []string, chunkSize int) [][]string {
function compCRC32 (line 229) | func compCRC32(f1 string, f2 string) bool {
function checkGnerateSIDFromLID (line 258) | func checkGnerateSIDFromLID(ld *msbimport.LineData) string {
function guessInputStickerFormat (line 299) | func guessInputStickerFormat(f string) string {
FILE: core/webapp.go
function InitWebAppServer (line 26) | func InitWebAppServer() {
function apiExport (line 56) | func apiExport(c *gin.Context) {
type webappStickerSet (line 65) | type webappStickerSet struct
type webappStickerObject (line 81) | type webappStickerObject struct
function apiSS (line 104) | func apiSS(c *gin.Context) {
function apiEditResult (line 183) | func apiEditResult(c *gin.Context) {
function commitEmojiChange (line 226) | func commitEmojiChange(ud *UserData, so []webappStickerObject) error {
function apiEditMove (line 266) | func apiEditMove(c *gin.Context) {
function apiInitData (line 291) | func apiInitData(c *gin.Context) {
function initWebAppRequest (line 311) | func initWebAppRequest(c *gin.Context) {
function validateHMAC (line 355) | func validateHMAC(dataCheckString string, hash string) bool {
function checkGetUd (line 374) | func checkGetUd(uid string, qid string) (*UserData, error) {
function prepareWebAppEditStickers (line 385) | func prepareWebAppEditStickers(ud *UserData) error {
function prepareWebAppExportStickers (line 409) | func prepareWebAppExportStickers(ss *tele.StickerSet, hex string) error {
FILE: core/workers.go
function initWorkersPool (line 14) | func initWorkersPool() {
function wDownloadStickerObject (line 21) | func wDownloadStickerObject(i interface{}) {
function wSubmitSMove (line 91) | func wSubmitSMove(i interface{}) {
FILE: pkg/msbimport/convert.go
constant FORMAT_TG_REGULAR_STATIC (line 23) | FORMAT_TG_REGULAR_STATIC = "tg_reg_static"
constant FORMAT_TG_EMOJI_STATIC (line 24) | FORMAT_TG_EMOJI_STATIC = "tg_emoji_static"
constant FORMAT_TG_REGULAR_ANIMATED (line 25) | FORMAT_TG_REGULAR_ANIMATED = "tg_reg_ani"
constant FORMAT_TG_EMOJI_ANIMATED (line 26) | FORMAT_TG_EMOJI_ANIMATED = "tg_emoji_ani"
constant KB (line 32) | KB = 1000
constant MB (line 33) | MB = 1000 * KB
constant GB (line 34) | GB = 1000 * MB
constant TB (line 35) | TB = 1000 * GB
constant PB (line 36) | PB = 1000 * TB
constant KiB (line 39) | KiB = 1024
constant MiB (line 40) | MiB = 1024 * KiB
constant GiB (line 41) | GiB = 1024 * MiB
constant TiB (line 42) | TiB = 1024 * GiB
constant PiB (line 43) | PiB = 1024 * TiB
function InitConvert (line 49) | func InitConvert() {
function CheckDeps (line 69) | func CheckDeps() []string {
function IMToWebpTGStatic (line 92) | func IMToWebpTGStatic(f string, isCustomEmoji bool) (string, error) {
function IMToWebpWA (line 125) | func IMToWebpWA(f string) error {
function IMToPng (line 153) | func IMToPng(f string) (string, error) {
function IMToApng (line 167) | func IMToApng(f string) (string, error) {
function ConverMediaToTGStickerSmart (line 183) | func ConverMediaToTGStickerSmart(f string, isCustomEmoji bool) (string, ...
function FFToWebmTGVideo (line 221) | func FFToWebmTGVideo(f string, isCustomEmoji bool) (string, error) {
function FFToWebmSafe (line 275) | func FFToWebmSafe(f string, isCustomEmoji bool) (string, error) {
function FFToGif (line 294) | func FFToGif(f string) (string, error) {
function IMStackToWebp (line 339) | func IMStackToWebp(base string, overlay string) (string, error) {
function RlottieToGIF (line 356) | func RlottieToGIF(f string) (string, error) {
function IMToAnimatedWebpLQ (line 401) | func IMToAnimatedWebpLQ(f string) error {
function FFToAnimatedWebpLQ (line 416) | func FFToAnimatedWebpLQ(f string) error {
function FFToAnimatedWebpWA (line 435) | func FFToAnimatedWebpWA(f string) error {
function FFtoPNG (line 484) | func FFtoPNG(f string, pathOut string) error {
function IMToPNGThumb (line 499) | func IMToPNGThumb(f string) error {
function SetImageTime (line 524) | func SetImageTime(f string, t time.Time) error {
FILE: pkg/msbimport/import.go
function ParseImportLink (line 18) | func ParseImportLink(link string, ld *LineData) (string, error) {
function PrepareImportStickers (line 46) | func PrepareImportStickers(ctx context.Context, ld *LineData, workDir st...
function convertSToTGFormat (line 59) | func convertSToTGFormat(ctx context.Context, ld *LineData) {
FILE: pkg/msbimport/import_kakao.go
function parseKakaoLink (line 18) | func parseKakaoLink(link string, ld *LineData) (string, error) {
function fetchKakaoMetadata (line 66) | func fetchKakaoMetadata(kakaoJson *KakaoJson, kakaoID string) error {
function prepareKakaoStickers (line 85) | func prepareKakaoStickers(ctx context.Context, ld *LineData, workDir str...
function prepareKakaoZipStickers (line 130) | func prepareKakaoZipStickers(ctx context.Context, ld *LineData, workDir ...
function kakaoZipExtract (line 171) | func kakaoZipExtract(f string, ld *LineData) []string {
function fetchKakaoDetailsFromShareLink (line 191) | func fetchKakaoDetailsFromShareLink(link string) (string, string, error) {
FILE: pkg/msbimport/import_line.go
function parseLineLink (line 18) | func parseLineLink(link string, ld *LineData) (string, error) {
function fetchLineI18nLinks (line 112) | func fetchLineI18nLinks(doc *goquery.Document) []string {
function fetchLineI18nTitles (line 136) | func fetchLineI18nTitles(ld *LineData) {
function parseLineDetails (line 172) | func parseLineDetails(doc *goquery.Document, lj *LineJson) error {
function prepareLineStickers (line 230) | func prepareLineStickers(ctx context.Context, ld *LineData, workDir stri...
function lineZipExtract (line 271) | func lineZipExtract(f string, ld *LineData) []string {
function sanitizeLinePNGs (line 303) | func sanitizeLinePNGs(files []string) bool {
function removeAPNGtEXtChunk (line 314) | func removeAPNGtEXtChunk(f string) bool {
function prepareLineMessageS (line 365) | func prepareLineMessageS(ctx context.Context, ld *LineData, workDir stri...
function parseLineProductInfo (line 421) | func parseLineProductInfo(id string, ld *LineData) error {
FILE: pkg/msbimport/typedefs.go
constant LINE_STICKER_STATIC (line 9) | LINE_STICKER_STATIC = "line_s"
constant LINE_STICKER_ANIMATION (line 10) | LINE_STICKER_ANIMATION = "line_a"
constant LINE_STICKER_POPUP (line 11) | LINE_STICKER_POPUP = "line_p"
constant LINE_STICKER_POPUP_EFFECT (line 12) | LINE_STICKER_POPUP_EFFECT = "line_f"
constant LINE_EMOJI_STATIC (line 13) | LINE_EMOJI_STATIC = "line_e"
constant LINE_EMOJI_ANIMATION (line 14) | LINE_EMOJI_ANIMATION = "line_i"
constant LINE_STICKER_MESSAGE (line 15) | LINE_STICKER_MESSAGE = "line_m"
constant LINE_STICKER_NAME (line 16) | LINE_STICKER_NAME = "line_n"
constant KAKAO_EMOTICON (line 17) | KAKAO_EMOTICON = "kakao_e"
constant LINE_SRC_PER_STICKER_TEXT (line 19) | LINE_SRC_PER_STICKER_TEXT = "PER_STICKER_TEXT"
constant LINE_SRC_ANIMATION (line 20) | LINE_SRC_ANIMATION = "ANIMATION"
constant LINE_SRC_STATIC (line 21) | LINE_SRC_STATIC = "STATIC"
constant LINE_SRC_POPUP (line 22) | LINE_SRC_POPUP = "POPUP"
constant LINE_SRC_NAME_TEXT (line 23) | LINE_SRC_NAME_TEXT = "NAME_TEXT"
constant LINE_POPUP_LAYER_BACKGROUND (line 26) | LINE_POPUP_LAYER_BACKGROUND = "BACKGROUND"
constant LINE_POPUP_LAYER_FOREGROUND (line 28) | LINE_POPUP_LAYER_FOREGROUND = "FOREGROUND"
constant StoreLine (line 30) | StoreLine = "line"
constant StoreKakao (line 31) | StoreKakao = "kakao"
constant WARN_KAKAO_PREFER_SHARE_LINK (line 33) | WARN_KAKAO_PREFER_SHARE_LINK = "prefer share link for kakao"
type LineFile (line 36) | type LineFile struct
type LineData (line 51) | type LineData struct
type LineJson (line 83) | type LineJson struct
type KakaoJsonResult (line 89) | type KakaoJsonResult struct
type KakaoJson (line 102) | type KakaoJson struct
type LineProductInfo (line 106) | type LineProductInfo struct
type Author (line 119) | type Author struct
type Price (line 130) | type Price struct
type Sticker (line 137) | type Sticker struct
type Title (line 143) | type Title struct
FILE: pkg/msbimport/util.go
function httpDownload (line 23) | func httpDownload(link string, f string) error {
function httpDownloadCurlUA (line 35) | func httpDownloadCurlUA(link string, f string) error {
function httpGet (line 49) | func httpGet(link string) (string, error) {
function httpGetWithRedirLink (line 63) | func httpGetWithRedirLink(link string) (string, string, error) {
function httpGetAndroidUA (line 77) | func httpGetAndroidUA(link string) (string, error) {
function fDownload (line 89) | func fDownload(link string, savePath string) error {
function fExtract (line 95) | func fExtract(f string) string {
function SecHex (line 109) | func SecHex(n int) string {
function ArchiveExtract (line 115) | func ArchiveExtract(f string) []string {
function LsFilesR (line 127) | func LsFilesR(dir string, mustHave []string, mustNotHave []string) []str...
function LsFiles (line 164) | func LsFiles(dir string, mustHave []string, mustNotHave []string) []stri...
function FCompress (line 193) | func FCompress(f string, flist []string) error {
function FCompressVol (line 212) | func FCompressVol(f string, flist []string) []string {
FILE: pkg/msbimport/workers.go
function wConvertWebm (line 14) | func wConvertWebm(i interface{}) {
FILE: tools/msb_emoji.py
function main (line 18) | def main():
FILE: tools/msb_kakao_decrypt.py
function generate_lfsr (line 12) | def generate_lfsr(key):
function xor_byte (line 33) | def xor_byte(b, seq):
function xor_data (line 61) | def xor_data(data):
function main (line 69) | def main():
FILE: tools/msb_rlottie.py
function main (line 13) | def main():
FILE: web/webapp3/src/App.js
function App (line 7) | function App() {
FILE: web/webapp3/src/Edit.js
function Edit (line 26) | function Edit() {
FILE: web/webapp3/src/Export.js
function Export (line 9) | function Export() {
FILE: web/webapp3/src/SortableSticker.js
function SortableSticker (line 6) | function SortableSticker(props) {
FILE: web/webapp3/src/StickerGrid.js
function StickerGrid (line 3) | function StickerGrid({children, columns}) {
FILE: web/webapp3/src/utils.js
function sha256sum (line 2) | function sha256sum(string) {
Condensed preview — 64 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (385K chars).
[
{
"path": ".github/workflows/build_binaries.yml",
"chars": 2628,
"preview": "name: Build binaries for moe-sticker-bot and msbimport\n\non: push\n\njobs:\n build_msb:\n runs-on: ubuntu-latest\n step"
},
{
"path": ".github/workflows/msb_nginx.yml",
"chars": 767,
"preview": "name: Build nginx for @moe-sticker-bot\n\non:\n push:\n branches: [ master ]\n pull_request:\n branches: [ master ]\n\n "
},
{
"path": ".github/workflows/msb_nginx_aarch64.sh",
"chars": 656,
"preview": "#!/usr/bin/bash\nGITHUB_TOKEN=$1\n\nbuildah login -u star-39 -p $GITHUB_TOKEN ghcr.io\n\n#AArch64\nc1=$(buildah from --arch=ar"
},
{
"path": ".github/workflows/msb_nginx_amd64.sh",
"chars": 705,
"preview": "#!/usr/bin/bash\nGITHUB_TOKEN=$1\n\nbuildah login -u star-39 -p $GITHUB_TOKEN ghcr.io\n\n#AMD64\nc1=$(buildah from docker://ng"
},
{
"path": ".github/workflows/msb_oci.yml",
"chars": 876,
"preview": "name: Build OCI container for moe-sticker-bot\n\non:\n push:\n branches: [ master ]\n pull_request:\n branches: [ mast"
},
{
"path": ".github/workflows/msb_oci_aarch64.sh",
"chars": 1678,
"preview": "#!/bin/sh\n\nGITHUB_TOKEN=$1\n\nbuildah login -u star-39 -p $GITHUB_TOKEN ghcr.io\n\n# AArch64\n###############################"
},
{
"path": ".github/workflows/msb_oci_amd64.sh",
"chars": 1622,
"preview": "#!/bin/sh\n\nGITHUB_TOKEN=$1\n\nbuildah login -u star-39 -p $GITHUB_TOKEN ghcr.io\n\n# AMD64\n#################################"
},
{
"path": ".github/workflows/release_note.md",
"chars": 868,
"preview": "See full changelog on https://github.com/star-39/moe-sticker-bot#changelog\n\n__moe-sticker-bot*__ is the binary for the b"
},
{
"path": ".gitignore",
"chars": 64,
"preview": ".vscode\n.env\n.venv\n__debug*\n*_data/\n\n.DS_Store\ndeploy.yaml\ntmp/\n"
},
{
"path": "LICENSE",
"chars": 35149,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
},
{
"path": "README.md",
"chars": 18054,
"preview": "# [@moe_sticker_bot](https://t.me/moe_sticker_bot)\n\n[\n\nfunc cmdSitRep(c tele.Context) error {\n\t// Report status.\n\t// stat"
},
{
"path": "core/commands.go",
"chars": 2067,
"preview": "package core\n\nimport (\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\ttele \"gopkg.in/telebot.v3\"\n)\n\nfunc cmdCreate(c tel"
},
{
"path": "core/config.go",
"chars": 527,
"preview": "package core\n\nvar msbconf ConfigTemplate\n\ntype ConfigTemplate struct {\n\tAdminUid int64\n\tDataDir string\n\tLogLevel string"
},
{
"path": "core/database.go",
"chars": 10858,
"preview": "package core\n\nimport (\n\t\"database/sql\"\n\t\"strings\"\n\n\t\"github.com/go-sql-driver/mysql\"\n\tlog \"github.com/sirupsen/logrus\"\n)"
},
{
"path": "core/define.go",
"chars": 4768,
"preview": "package core\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/go-co-op/gocron\"\n\t\"github.com/panjf2000/ants/v2\"\n\t\"github.com/st"
},
{
"path": "core/init.go",
"chars": 4961,
"preview": "package core\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime/debug\"\n\t\"time\"\n\n\t\"github.com/go-co-op/gocron\"\n\t"
},
{
"path": "core/message.go",
"chars": 29080,
"preview": "package core\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tlog"
},
{
"path": "core/os_util.go",
"chars": 1185,
"preview": "package core\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc purgeOutdatedStorageDat"
},
{
"path": "core/states.go",
"chars": 13553,
"preview": "package core\n\nimport (\n\t\"errors\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"git"
},
{
"path": "core/sticker.go",
"chars": 16171,
"preview": "package core\n\nimport (\n\t\"errors\"\n\t\"math/rand\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sir"
},
{
"path": "core/sticker_download.go",
"chars": 4619,
"preview": "package core\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/panjf2000/ants/v2\"\n\t\"githu"
},
{
"path": "core/userdata.go",
"chars": 3041,
"preview": "package core\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/star"
},
{
"path": "core/util.go",
"chars": 6450,
"preview": "package core\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"hash/crc32\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exe"
},
{
"path": "core/webapp.go",
"chars": 10951,
"preview": "package core\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\""
},
{
"path": "core/workers.go",
"chars": 2523,
"preview": "package core\n\nimport (\n\t\"strings\"\n\n\t\"github.com/panjf2000/ants/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/star-3"
},
{
"path": "deployments/kubernetes_msb.yaml",
"chars": 2330,
"preview": "# Save the output of this file and use kubectl create -f to import\n# it into Kubernetes.\n#\n# Created with podman-4.2.0\na"
},
{
"path": "go.mod",
"chars": 2426,
"preview": "module github.com/star-39/moe-sticker-bot\n\ngo 1.19\n\nreplace gopkg.in/telebot.v3 => github.com/star-39/telebot/v3 v3.99.9"
},
{
"path": "go.sum",
"chars": 100313,
"preview": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1"
},
{
"path": "pkg/msbimport/README.md",
"chars": 3206,
"preview": "# Moe-Sticker-Bot Import Component (msbimport)\n\n[\n\n// This fun"
},
{
"path": "pkg/msbimport/import_kakao.go",
"chars": 5320,
"preview": "package msbimport\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t"
},
{
"path": "pkg/msbimport/import_line.go",
"chars": 11441,
"preview": "package msbimport\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"str"
},
{
"path": "pkg/msbimport/typedefs.go",
"chars": 3764,
"preview": "package msbimport\n\nimport (\n\t\"sync\"\n)\n\n// Line sticker types\nconst (\n\tLINE_STICKER_STATIC = \"line_s\" //普通貼圖\n\tLINE"
},
{
"path": "pkg/msbimport/util.go",
"chars": 5991,
"preview": "package msbimport\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/file"
},
{
"path": "pkg/msbimport/workers.go",
"chars": 718,
"preview": "package msbimport\n\nimport (\n\t\"strings\"\n\n\t\"github.com/panjf2000/ants/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Workers "
},
{
"path": "test/kakao_links.json",
"chars": 339,
"preview": "[\n [\n \"static kakao store\",\n \"https://e.kakao.com/t/pretty-all-friends\"\n ],\n [\n \"animated "
},
{
"path": "test/line_links.json",
"chars": 1165,
"preview": "[\n [\n \"LINE_STICKER_ANIMATED\",\n \"https://store.line.me/stickershop/product/8831/\"\n ],\n [\n "
},
{
"path": "test/tg_links.json",
"chars": 334,
"preview": "[\n [\n \"Static\",\n \"https://t.me/addstickers/sticker_6356d1bf_by_moe_sticker_bot\"\n "
},
{
"path": "tools/msb_emoji.py",
"chars": 773,
"preview": "#!/usr/bin/python3\n\nimport emoji\nimport sys\nimport json\n\n# This simple python tool utilizes\n# 'emoji' package in PyPI wh"
},
{
"path": "tools/msb_kakao_decrypt.py",
"chars": 1980,
"preview": "#!/usr/bin/python3\n\n# This tool is intended to decrypt kakao animated webp sticker.\n# Specify file path as 1st pos arg a"
},
{
"path": "tools/msb_rlottie.py",
"chars": 402,
"preview": "#!/usr/bin/python3\n\n# Utilize rlottie-python to convert TGS images.\n# Credit https://github.com/laggykiller/rlottie-pyth"
},
{
"path": "web/nginx/assetlinks.json",
"chars": 395,
"preview": "[\n {\n \"relation\": [\n \"delegate_permission/common.handle_all_urls\"\n ],\n \"target\": {\n "
},
{
"path": "web/nginx/default.conf.template",
"chars": 880,
"preview": "# For more information on configuration, see:\n# * Official English Documentation: http://nginx.org/en/docs/\n# * Offi"
},
{
"path": "web/webapp3/.gitignore",
"chars": 310,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "web/webapp3/README.md",
"chars": 159,
"preview": "# WebApp for moe-sticker-bot\n\nSupports editing emoji and drag n drop to sort.\n\nSupports dark mode.\n\n## Build\n```\nREACT_A"
},
{
"path": "web/webapp3/package.json",
"chars": 841,
"preview": "{\n \"name\": \"webapp3\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"dependencies\": {\n \"@dnd-kit/core\": \"^6.1.0\",\n \""
},
{
"path": "web/webapp3/public/index.html",
"chars": 1868,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.i"
},
{
"path": "web/webapp3/public/manifest.json",
"chars": 492,
"preview": "{\n \"short_name\": \"React App\",\n \"name\": \"Create React App Sample\",\n \"icons\": [\n {\n \"src\": \"favicon.ico\",\n "
},
{
"path": "web/webapp3/public/robots.txt",
"chars": 67,
"preview": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
},
{
"path": "web/webapp3/src/App.css",
"chars": 1,
"preview": "\n"
},
{
"path": "web/webapp3/src/App.js",
"chars": 1381,
"preview": "import { useEffect, useState } from 'react';\nimport Edit from './Edit'\nimport Export from './Export';\nimport axios from "
},
{
"path": "web/webapp3/src/Edit.js",
"chars": 5389,
"preview": "import React, { useEffect, useReducer, useState } from 'react';\nimport axios from 'axios';\n\nimport {\n DndContext,\n clo"
},
{
"path": "web/webapp3/src/Export.js",
"chars": 2257,
"preview": "import React, { useEffect, useReducer, useState } from 'react';\nimport axios, { all } from 'axios';\nimport { sha256sum }"
},
{
"path": "web/webapp3/src/SortableSticker.js",
"chars": 549,
"preview": "import {useSortable} from '@dnd-kit/sortable';\nimport {CSS} from '@dnd-kit/utilities';\n\nimport {Sticker} from './Sticker"
},
{
"path": "web/webapp3/src/Sticker.js",
"chars": 734,
"preview": "import React, { forwardRef } from 'react';\n// import axios from 'axios';\nimport Img from \"react-cool-img\";\nimport './Sti"
},
{
"path": "web/webapp3/src/StickerGrid.js",
"chars": 321,
"preview": "import React from 'react';\n\nexport function StickerGrid({children, columns}) {\n return (\n <ul\n style={{\n "
},
{
"path": "web/webapp3/src/StickerStyle.css",
"chars": 206,
"preview": "img {\n display: block;\n max-width: 64px;\n max-height: 64px;\n width: auto;\n height: auto;\n}\n\n.Image-div {\n"
},
{
"path": "web/webapp3/src/index.css",
"chars": 2381,
"preview": ".div {\n touch-action: auto;\n}\n\nbody {\n font-family: sans-serif;\n background-color: var(--tg-theme-bg-color, #ffffff);"
},
{
"path": "web/webapp3/src/index.js",
"chars": 353,
"preview": "import React, { StrictMode } from 'react';\nimport ReactDOM from 'react-dom/client';\nimport './index.css';\nimport App fro"
},
{
"path": "web/webapp3/src/utils.js",
"chars": 381,
"preview": "\nexport function sha256sum(string) {\n const utf8 = new TextEncoder().encode(string);\n return crypto.subtle.digest("
}
]
About this extraction
This page contains the full source code of the star-39/moe-sticker-bot GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 64 files (347.2 KB), approximately 130.1k tokens, and a symbol index with 316 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.