Compare commits

...

60 Commits
v1.0 ... master

Author SHA1 Message Date
Maxim Lebedev 41fcfd5f51
🔀 Merge branch 'develop' 2018-06-25 01:12:46 +05:00
Maxim Lebedev 6add28126b
🚨 Removed linter warnings 2018-06-25 01:11:47 +05:00
Maxim Lebedev 2c730eb012
🔥 Removed useless files and directories 2018-06-25 01:00:55 +05:00
Maxim Lebedev c2ea3cea36
👷 Added GitLab CI configs 2018-06-25 00:56:55 +05:00
Maxim Lebedev c086448a32
🚚 Moving to GitLab 2018-06-24 22:37:20 +05:00
Maxim Lebedev ade07764c0
♻️ Refactoring code due new telegram package changes 2018-04-19 20:16:17 +05:00
Maxim Lebedev 638dd0f6b6
🎨 Small fixes and format of the code 2018-03-31 01:20:00 +05:00
Maxim Lebedev 79d45575a0
🔥 Removed pack size stats
I add /stats command and counters later
2018-03-31 01:19:15 +05:00
Maxim Lebedev 45c0c93ea9
💚 Fixed Makefile for success Travis test 2018-03-30 23:57:45 +05:00
Maxim Lebedev 4142fadbef
🚚 Move LICENSE into root 2018-03-26 18:14:17 +05:00
Maxim Lebedev ebfabb48be
🔀 Merge branch 'develop' 2018-03-26 18:12:43 +05:00
Maxim Lebedev 5ab333b5ae
👥 Updated contributors list 2018-03-26 18:12:13 +05:00
Maxim Lebedev 042c76ec37
🚚 Rename 'translations' directory to 'i18n' 2018-03-26 18:11:48 +05:00
Maxim Lebedev 07aaebdc65
🚨 Removed linter warnings 2018-03-26 18:10:36 +05:00
Maxim Lebedev db67a2db19
🔀 Merge branch 'develop'
Refactoring of repository content and minor fixes
2018-02-20 20:56:10 +05:00
Maxim Lebedev 2df726ad32
💚 Returned Travis CI config into root 2018-02-20 20:45:51 +05:00
Maxim Lebedev 9acb2c77df
🐛 Fixed badge link to LICENSE in docs/README.md 2018-02-20 20:39:37 +05:00
Maxim Lebedev a65ca2e796
🏗️ Maked architectural changes 2018-02-20 20:34:50 +05:00
Maxim Lebedev 4aaff889ea
♻️ Use new telegram package helpers 2018-02-03 08:08:59 +05:00
Maxim Lebedev 0400833ade
✏️ Fixed invalid localization key names 2018-02-03 08:08:34 +05:00
Maxim Lebedev 97759d7cbf
🎨 Little code format 2018-01-25 17:26:10 +05:00
Maxim Lebedev 4dfb50b346
🔊 Write panic logs to journals 2018-01-25 17:25:54 +05:00
Maxim Lebedev 7c6aa96eff
Wait sending announcements to all users before panic 2018-01-25 17:25:13 +05:00
Maxim Lebedev 27611490a5
♻️ Use new package helpers 2018-01-25 17:24:04 +05:00
Maxim Lebedev dfa0844bd5
🔀 Merge branch 'release/1.2' 2018-01-20 03:10:21 +05:00
Maxim Lebedev 88dc6f7ff5
🔀 Merge branch 'release/1.2' into develop 2018-01-20 03:09:27 +05:00
Maxim Lebedev 74430226b9
🔖 Releasing v1.2
With buttons-menu, improved searching and 'delPack' command!
2018-01-20 03:00:06 +05:00
Maxim Lebedev 6ade04cfc9
♻️ Refactoring codewith adding some small features
Remove linter warnings, changed inline-feed-button command, added message with switch-button. Also, fixed #1
2018-01-20 01:41:16 +05:00
Maxim Lebedev 1f53b751f8
Improve searching stickers via fixing emoji runes
Fixed #1
2018-01-19 22:59:07 +05:00
Maxim Lebedev a603eea78d
🚚 Moved checking of sticker from commands to actions 2018-01-19 19:43:26 +05:00
Maxim Lebedev 471a30d6d5
🚨 Removed linter warnings 2018-01-19 18:02:46 +05:00
Maxim Lebedev 57a0ff0985
♻️ Replaced lost hand-maked keyboard on keyboard helper 2018-01-19 01:56:01 +05:00
Maxim Lebedev dcef1ebbf7
🐛 Fixed user language fallback 2018-01-18 04:51:23 +05:00
Maxim Lebedev ccf7a63be2
🚸 Improve user experience by adding buttons menu
Need to return inline-buttons for fast calling bot pack
2018-01-18 04:50:47 +05:00
Maxim Lebedev 133a271831
Added '/delPack' command support
This command delete whole set from user pack by of getted sticker. Close #4
2018-01-18 02:05:43 +05:00
Maxim Lebedev 957bc2b4ac
🚑 Fixed invalid localization tag-names 2017-12-28 18:00:22 +05:00
Maxim Lebedev 5a82fa554e
🌐 Added translated reset key phrase 2017-12-28 17:48:15 +05:00
Maxim Lebedev bdae4cd9f6
🌐 Added russian language support 2017-12-28 17:41:53 +05:00
Maxim Lebedev 6766cd827a
🐛 Fixed fallback locale from 'en-us' to 'en' 2017-12-28 16:46:22 +05:00
Maxim Lebedev 8d78126d2a
🐛 Fixed localization actions in Makefile 2017-12-28 16:45:25 +05:00
Maxim Lebedev ce269401de
🏗️ Organize localization files architecture 2017-12-28 16:44:54 +05:00
Maxim Lebedev 320618e0a3
🔀 Merge branch 'release/1.1' into develop 2017-12-28 16:19:01 +05:00
Maxim Lebedev 5d2bb60d8b
💡 Updated README.md 2017-12-28 15:37:30 +05:00
Maxim Lebedev f57f7a49b0
📄 Update license from MIT to Apache 2.0 2017-12-28 15:36:21 +05:00
Maxim Lebedev 9bd458b0ef
💚 Fixed 'go get' commands in .travis.yml 2017-12-28 12:33:38 +05:00
Maxim Lebedev 5e2b3b7f61
🚸 Move uploaded stickers to up 2017-12-27 21:27:22 +05:00
Maxim Lebedev cb4189a1ed
Wipe all old stickers keys (sorry) 2017-12-27 21:26:41 +05:00
Maxim Lebedev 62ba8b5634
Added news/announcement message reposts from bot channel 2017-12-27 21:17:29 +05:00
Maxim Lebedev 5b6ce166de
🐛 Fixed package name in methods and other actions 2017-12-27 21:15:54 +05:00
Maxim Lebedev 3aa78de268
🐛 Fixed parameters in add/remove sticker methods 2017-12-27 21:13:20 +05:00
Maxim Lebedev 98f9d2d8d1
🗃️ Added dbGetUsers method 2017-12-27 21:11:19 +05:00
Maxim Lebedev 4e4fd28372
🗃️ Added set name in stickers keys 2017-12-27 21:10:59 +05:00
Maxim Lebedev 18d0aac5de
🚚 Renamed telegram package import 2017-12-27 20:26:41 +05:00
Maxim Lebedev 0880f2a5dc
Revert " Set cacheTime from 1 to 300 seconds"
This reverts commit e8713303c5.
2017-12-27 19:36:05 +05:00
Maxim Lebedev e8713303c5
Set cacheTime from 1 to 300 seconds 2017-12-20 03:55:55 +05:00
Maxim Lebedev 144bb88f87
🎨 Little code format for cases 2017-12-20 03:55:30 +05:00
Maxim Lebedev a43c39de8a
👥 Updated PATRONS.md list
https://patreon.com/toby3d
2017-12-08 04:33:12 +05:00
Maxim Lebedev f448718826
🔥 Removed @botanio API token from configuration file 2017-12-08 04:13:39 +05:00
Maxim Lebedev 2de9abf8e4
Improving searching and getting stickers performance
Read and count all user stickers, but return only 50 (+1 for checking next page)
2017-12-08 04:12:26 +05:00
Maxim Lebedev 6de6088cdb
🔥 RIP @botanio :( 2017-12-08 04:09:50 +05:00
86 changed files with 2164 additions and 1271 deletions

10
.gitignore vendored
View File

@ -11,13 +11,13 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
# Project-local glide cache
# RE: https://github.com/Masterminds/glide/issues/736
.glide/
# Golang project vendor packages which should be ignored
vendor/
# Production files must not be in public repository
config.yaml
MyPackBot
stickers.db
configs/config.yaml
cert.key
cert.pem

47
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,47 @@
image: golang:alpine
cache:
paths:
- /go/src/github.com
- /go/src/gitlab.com
- /go/src/golang.org
- /go/src/google.golang.org
- /go/src/gopkg.in
stages:
- test
- build
before_script:
- apk add --no-cache git build-base bash
- mkdir -p /go/src/gitlab.com/$CI_PROJECT_NAMESPACE /go/src/_/builds
- cp -r $CI_PROJECT_DIR /go/src/gitlab.com/$CI_PROJECT_PATH
- ln -s /go/src/gitlab.com/$CI_PROJECT_NAMESPACE /go/src/_/builds/$CI_PROJECT_NAMESPACE
- make dep
unit_tests:
stage: test
script:
- make test
.race_detector:
stage: test
script:
- make race
code_coverage:
stage: test
script:
- make coverage
lint_code:
stage: test
script:
- go get github.com/go-critic/go-critic/cmd/gocritic
- go install github.com/go-critic/go-critic/cmd/gocritic
- make lint
build:
stage: build
script:
- make

View File

@ -1,8 +0,0 @@
language: go
go:
- 1.9
- tip
install:
- go get

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,20 +0,0 @@
# MIT License
Copyright (c) 2017 Maxim Lebedev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,26 +1,32 @@
# Build package by default with all general tools and things
all:
make localization
make build
PROJECT_NAMESPACE := $(CI_PROJECT_NAMESPACE)
PROJECT_NAME := $(CI_PROJECT_NAME)
PROJECT_PATH := $(PROJECT_NAMESPACE)/$(PROJECT_NAME)
PACKAGE_NAME := "gitlab.com/$(PROJECT_PATH)"
PACKAGE_PATH := $(GOPATH)/src/$(PACKAGE_NAME)
PACKAGE_LIST := $(shell go list $(PACKAGE_NAME)/... | grep -v /vendor/)
GO_FILES := $(shell find . -name '*.go' | grep -v /vendor/ | grep -v _test.go)
# Build minimal package only
build:
go build
.PHONY: all lint test rase coverage dep build clean
# Build debug version with more logs
debug:
go build -tags=debug
all: build
# Format the source code
fmt:
go fmt
lint: ## Lint the files
@gocritic check-project $(PACKAGE_PATH)
# Build localization files with separated untranslated strings
translation:
goi18n merge -format yaml -sourceLanguage en-us -outdir ./i18n/ ./i18n/*
race: dep ## Run data race detector
@go test -race -short ${PACKAGE_LIST}
# Build localization files and merge untranslated strings
localization:
make translation
goi18n -format yaml -sourceLanguage en-us -outdir ./i18n/ ./i18n/*.all.yaml \
./i18n/*.untranslated.yaml
coverage: ## Generate global code coverage report
@go test -cover -v -coverpkg=$(PACKAGE_NAME) ${PACKAGE_LIST}
dep: ## Get the dependencies
@go get -v -d -t ${PACKAGE_LIST}
build: dep ## Build the binary file
@go build -i -v $(PACKAGE_NAME)
clean: ## Remove previous build
@rm -f $(PROJECT_NAME)
help: ## Display this help screen
@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

View File

@ -1,7 +0,0 @@
# [Patrons!](https://www.patreon.com/bePatron?c=243288)
**These people have sponsored the current version of the project:**
- **Ivan Kolesnikov**
- Daniil Tlenov
- Aurielb
- Yami Odymel
- MoD21k

View File

@ -1 +0,0 @@
# [@MyPackBot](https://t.me/MyPackBot) [![discord](https://discordapp.com/api/guilds/208605007744860163/widget.png)](https://discord.gg/KYQB9FR)

107
add.go
View File

@ -1,107 +0,0 @@
package main
import (
log "github.com/kirillDanshin/dlog" // Insert logs only in debug builds
"github.com/toby3d/go-telegram" // My Telegram bindings
)
func commandAdd(msg *telegram.Message, pack bool) {
bot.SendChatAction(msg.Chat.ID, telegram.ActionTyping)
T, err := switchLocale(msg.From.LanguageCode)
errCheck(err)
reply := telegram.NewMessage(msg.Chat.ID, T("reply_add_sticker"))
reply.ParseMode = telegram.ModeMarkdown
err = dbChangeUserState(msg.From.ID, stateAddSticker)
errCheck(err)
if pack {
reply.Text = T("reply_add_pack")
err = dbChangeUserState(msg.From.ID, stateAddPack)
errCheck(err)
}
log.Ln("Sending add reply...")
_, err = bot.SendMessage(reply)
errCheck(err)
}
func actionAdd(msg *telegram.Message, pack bool) {
bot.SendChatAction(msg.Chat.ID, telegram.ActionTyping)
T, err := switchLocale(msg.From.LanguageCode)
errCheck(err)
reply := telegram.NewMessage(msg.Chat.ID, T("success_add_sticker"))
reply.ParseMode = telegram.ModeMarkdown
switch {
case pack && msg.Sticker.SetName == "":
reply.Text = T("error_empty_add_pack", map[string]interface{}{
"AddStickerCommand": cmdAddSticker,
})
case pack && msg.Sticker.SetName != "":
set, err := bot.GetStickerSet(msg.Sticker.SetName)
errCheck(err)
log.Ln("SetTitle:", set.Title)
reply.Text = T("success_add_pack", map[string]interface{}{
"SetTitle": set.Title,
})
allExists := true
for _, sticker := range set.Stickers {
exists, err := dbAddSticker(msg.From.ID, sticker.FileID, sticker.Emoji)
errCheck(err)
if !exists {
allExists = false
}
}
log.Ln("All exists?", allExists)
if allExists {
reply.Text = T("error_already_add_pack", map[string]interface{}{
"SetTitle": set.Title,
})
} else {
markup := telegram.NewInlineKeyboardMarkup(
telegram.NewInlineKeyboardRow(
telegram.NewInlineKeyboardButtonSwitch(
T("button_share"),
" ",
),
),
)
reply.ReplyMarkup = &markup
}
default:
exists, err := dbAddSticker(msg.From.ID, msg.Sticker.FileID, msg.Sticker.Emoji)
errCheck(err)
if exists {
reply.Text = T("error_already_add_sticker")
}
if msg.Sticker.Emoji == "" {
msg.Sticker.Emoji = " "
}
markup := telegram.NewInlineKeyboardMarkup(
telegram.NewInlineKeyboardRow(
telegram.NewInlineKeyboardButtonSwitch(
T("button_share"),
msg.Sticker.Emoji,
),
),
)
reply.ReplyMarkup = &markup
}
_, err = bot.SendMessage(reply)
errCheck(err)
}

View File

@ -1,34 +0,0 @@
package main
import "github.com/toby3d/go-telegram" // My Telegram bindings
func commandCancel(msg *telegram.Message) {
bot.SendChatAction(msg.Chat.ID, telegram.ActionTyping)
T, err := switchLocale(msg.From.LanguageCode)
errCheck(err)
state, err := dbGetUserState(msg.From.ID)
errCheck(err)
var text string
switch state {
case stateAddSticker:
text = T("cancel_add_sticker")
case stateAddPack:
text = T("cancel_add_pack")
case stateDelete:
text = T("cancel_del")
case stateReset:
text = T("cancel_reset")
default:
text = T("cancel_error")
}
err = dbChangeUserState(msg.From.ID, stateNone)
errCheck(err)
reply := telegram.NewMessage(msg.Chat.ID, text)
_, err = bot.SendMessage(reply)
errCheck(err)
}

View File

@ -1,111 +0,0 @@
package main
import (
"strings"
log "github.com/kirillDanshin/dlog" // Insert logs only in debug builds
"github.com/toby3d/botan"
"github.com/toby3d/go-telegram" // My Telegram bindings
)
const (
cmdAddPack = "addPack"
cmdAddSticker = "addSticker"
cmdCancel = "cancel"
cmdHelp = "help"
cmdDelete = "del"
cmdReset = "reset"
cmdStart = "start"
)
func commands(msg *telegram.Message) {
log.Ln("Received a", msg.Command(), "command")
switch strings.ToLower(msg.Command()) {
case strings.ToLower(cmdStart):
appMetrika.TrackAsync(
"Start", msg.From.ID, *msg,
func(answer *botan.Answer, err error) {
log.Ln("Asynchonous:", answer.Status)
metrika <- true
},
)
commandStart(msg)
<-metrika
case strings.ToLower(cmdHelp):
appMetrika.TrackAsync(
"Help", msg.From.ID, *msg,
func(answer *botan.Answer, err error) {
log.Ln("Asynchonous:", answer.Status)
metrika <- true
},
)
commandHelp(msg)
<-metrika
case strings.ToLower(cmdAddSticker):
appMetrika.TrackAsync(
"Add single sticker", msg.From.ID, *msg,
func(answer *botan.Answer, err error) {
log.Ln("Asynchonous:", answer.Status)
metrika <- true
},
)
commandAdd(msg, false)
<-metrika
case strings.ToLower(cmdAddPack):
appMetrika.TrackAsync(
"Add pack", msg.From.ID, *msg,
func(answer *botan.Answer, err error) {
log.Ln("Asynchonous:", answer.Status)
metrika <- true
},
)
commandAdd(msg, true)
<-metrika
case strings.ToLower(cmdDelete):
appMetrika.TrackAsync(
"Delete single sticker", msg.From.ID, *msg,
func(answer *botan.Answer, err error) {
log.Ln("Asynchonous:", answer.Status)
metrika <- true
},
)
commandDelete(msg)
<-metrika
case strings.ToLower(cmdReset):
appMetrika.TrackAsync(
"Reset", msg.From.ID, *msg,
func(answer *botan.Answer, err error) {
log.Ln("Asynchonous:", answer.Status)
metrika <- true
},
)
commandReset(msg)
<-metrika
case strings.ToLower(cmdCancel):
appMetrika.TrackAsync(
"Cancel", msg.From.ID, *msg,
func(answer *botan.Answer, err error) {
log.Ln("Asynchonous:", answer.Status)
metrika <- true
},
)
commandCancel(msg)
<-metrika
default:
appMetrika.Track("Command", msg.From.ID, *msg)
}
}

View File

@ -1,8 +1,7 @@
telegram:
token: 123456789:ABCd1efGhjKLM23O4pqR5stuvwx678yz90
webhook:
set: https://www.google.com
set: https://toby3d.github.io
listen: /bot
serve: 0.0.0.0:2368
botan: 12345a67-8b9c-01d2-e345-67fg8901hj23
channel: -1000000000000

View File

@ -1,167 +0,0 @@
package main
import (
"fmt"
"strconv"
"strings"
log "github.com/kirillDanshin/dlog" // Insert logs only in debug builds
"github.com/tidwall/buntdb" // Redis-like database
)
const (
stateNone = "none"
stateAddSticker = "addSticker"
stateAddPack = "addPack"
stateDelete = "del"
stateReset = "reset"
)
var db *buntdb.DB
func dbInit() {
log.Ln("Open database file...")
var err error
db, err = buntdb.Open("stickers.db")
errCheck(err)
log.Ln("Creating user_stickers index...")
err = db.CreateIndex(
"user_stickers", // name
"user:*:sticker:*", // pattern
buntdb.IndexString, // options
)
errCheck(err)
select {}
}
func dbChangeUserState(userID int, state string) error {
log.Ln("Trying to change", userID, "state to", state)
return db.Update(func(tx *buntdb.Tx) error {
_, _, err := tx.Set(
fmt.Sprint("user:", userID, ":state"), // key
state, // val
nil, // options
)
return err
})
}
func dbGetUserState(userID int) (string, error) {
log.Ln("Trying to get", userID, "state")
var state string
err := db.View(func(tx *buntdb.Tx) error {
var err error
state, err = tx.Get(fmt.Sprint("user:", userID, ":state"))
return err
})
switch err {
case buntdb.ErrNotFound:
log.Ln(userID, "not found, create new one")
if err := dbChangeUserState(userID, stateNone); err != nil {
return state, err
}
}
return state, err
}
func dbAddSticker(userID int, fileID, emoji string) (bool, error) {
log.Ln("Trying to add", fileID, "sticker from", userID, "user")
var exists bool
err := db.Update(func(tx *buntdb.Tx) error {
var err error
_, exists, err = tx.Set(
fmt.Sprint("user:", userID, ":sticker:", fileID), // key
emoji, // value
nil, // options
)
if err == buntdb.ErrIndexExists {
exists = true
return nil
}
return err
})
return exists, err
}
func dbDeleteSticker(userID int, fileID string) (bool, error) {
log.Ln("Trying to remove", fileID, "sticker from", userID, "user")
err := db.Update(func(tx *buntdb.Tx) error {
_, err := tx.Delete(fmt.Sprint("user:", userID, ":sticker:", fileID))
return err
})
switch err {
case buntdb.ErrNotFound:
log.Ln(userID, "not found, create new one")
return true, nil
}
return false, err
}
func dbResetUserStickers(userID int) error {
log.Ln("Trying reset all stickers of", userID, "user")
return db.Update(func(tx *buntdb.Tx) error {
var keys []string
err := tx.Ascend(
"user_stickers", // index
func(key, val string) bool { // iterator
subKeys := strings.Split(key, ":")
if subKeys[1] == strconv.Itoa(userID) {
keys = append(keys, key)
}
return true
},
)
if err != nil {
return err
}
for i := range keys {
_, err = tx.Delete(keys[i])
if err != nil {
break
}
}
return err
})
}
func dbGetUserStickers(userID int) ([]string, []string, error) {
log.Ln("Trying to get", userID, "stickers")
var stickers, emojis []string
err := db.View(func(tx *buntdb.Tx) error {
return tx.Ascend(
"user_stickers", // index
func(key, val string) bool { // iterator
subKeys := strings.Split(key, ":")
if subKeys[1] != strconv.Itoa(userID) {
return true
}
stickers = append(stickers, subKeys[3])
emojis = append(emojis, val)
return true
},
)
})
switch {
case err == buntdb.ErrNotFound:
log.Ln("Not found stickers")
return nil, nil, nil
case err != nil:
return nil, nil, err
}
return stickers, emojis, nil
}

View File

@ -1,62 +0,0 @@
package main
import "github.com/toby3d/go-telegram" // My Telegram bindings
func commandDelete(msg *telegram.Message) {
bot.SendChatAction(msg.Chat.ID, telegram.ActionTyping)
T, err := switchLocale(msg.From.LanguageCode)
errCheck(err)
stickers, _, err := dbGetUserStickers(msg.From.ID)
errCheck(err)
if len(stickers) <= 0 {
err = dbChangeUserState(msg.From.ID, stateNone)
errCheck(err)
reply := telegram.NewMessage(msg.Chat.ID, T("error_empty_remove"))
_, err = bot.SendMessage(reply)
errCheck(err)
return
}
err = dbChangeUserState(msg.From.ID, stateDelete)
errCheck(err)
markup := telegram.NewInlineKeyboardMarkup(
telegram.NewInlineKeyboardRow(
telegram.NewInlineKeyboardButtonSwitchSelf(
T("button_remove"),
" ",
),
),
)
reply := telegram.NewMessage(msg.Chat.ID, T("reply_remove"))
reply.ParseMode = telegram.ModeMarkdown
reply.ReplyMarkup = &markup
_, err = bot.SendMessage(reply)
errCheck(err)
}
func actionDelete(msg *telegram.Message) {
bot.SendChatAction(msg.Chat.ID, telegram.ActionTyping)
T, err := switchLocale(msg.From.LanguageCode)
errCheck(err)
notExist, err := dbDeleteSticker(msg.From.ID, msg.Sticker.FileID)
errCheck(err)
reply := telegram.NewMessage(msg.Chat.ID, T("success_remove"))
reply.ParseMode = telegram.ModeMarkdown
if notExist {
reply.Text = T("error_already_remove")
}
_, err = bot.SendMessage(reply)
errCheck(err)
}

View File

@ -7,7 +7,7 @@ services:
- 2368
volumes:
- ./MyPackBot:/go/MyPackBot
- ./i18n/:/go/i18n/
- ./config.yaml:/go/config.yaml
- ./translations/:/go/translations/
- ./configs/config.yaml:/go/configs/config.yaml
- ./stickers.db:/go/stickers.db
entrypoint: ["/go/MyPackBot", "-webhook"]

39
docs/CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,39 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at git@toby3d.ru. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

43
docs/CONTRIBUTING.md Normal file
View File

@ -0,0 +1,43 @@
# Code standarts
Standards help to keep the code readable and understandable, although they may seem strange or uncomfortable. Code style described below is not strict, but I give priority to those contributors who follow it. :heart:
## Rules
- Format the changes via `go fmt` before committing.
- Indents with tabs (with width 8), no spaces.
- In `import` external packages are separated from native by empty line.
- Maximum line lenght is 120 characters.
- Do not forget to comment what do you do.
- Check what you are writing, tests before commiting.
- Double check what you are writing, remove all [gometalinter](https://github.com/alecthomas/gometalinter) warnings.
## Guidance
Keep in mind the following before, while and after writing the code:
- **Less is always more.**
Write the least amount of code possible to solve just the problem at hand.
- **Predicting the future is impossible.**
Try to distinguish between anticipating potential future problems and potential future features. The former is usually good, the latter is usually bad.
- **Functional programming is functional.**
Functions should be small and single-purpose. Large variable lists are a sign your function does too much.
# Git workflow
`master` contains a stable version of the project, when as `develop` it is constantly updated and contains the latest changes. When proposing changes, you must specify `develop` as the target branch of PR.
## Commits
- First line of commit message should be to 80 chars long as public description of what you have achieved with the commit.
- Leave a blank line after the first line.
- The 3rd line can reference issue with `issue #000` if you just want to mention an issue or `closes #000` if your commit closes an issue. If you don't have an issue to reference or close, think carefully about whether you need to raise one before opening a PR.
- Use bullet points on the following lines to explain what your changes achieve and in particular why you've used your approach.
- Add a contextual emoji before commit title [based on its content](https://gitmoji.carloscuesta.me) (or [use appropriate tool for commiting](https://github.com/carloscuesta/gitmoji-cli)). This **greatly** helps visually to distinguish commits among themselves.
If you need to update your existing commit message, you can do this by running `git commit --amend` on your branch.
## Pull requests
The easier it is for me to merge a PR, the faster we'll be able to do it. Please take steps to make merging easy and keep the history clean and useful.
- **Always work on a branch.**
It will make your life much easier, really. Not touching the `master` branch will also simplify keeping your fork up-to-date.
- **Use issues properly.**
Bugs, changes and features are all different and should be treated differently. Use your commit message to close or reference issues. The more information you provide, the more likely your PR will get merged.
## Issues
Feel free to pick up any issue which is not assigned. Please leave a comment on the issue to say you wish to pick it up, and it will get assigned to you.

78
docs/README.md Normal file
View File

@ -0,0 +1,78 @@
# [@MyPackBot](https://t.me/MyPackBot) [![discord](https://discordapp.com/api/guilds/208605007744860163/widget.png)](https://discord.gg/KYQB9FR)
[![License](https://img.shields.io/crates/l/rustc-serialize.svg)](LICENSE)
[![Build Status](https://travis-ci.org/toby3d/MyPackBot.svg)](https://travis-ci.org/toby3d/MyPackBot)
[![Go Report](https://goreportcard.com/badge/github.com/toby3d/MyPackBot)](https://goreportcard.com/report/github.com/toby3d/MyPackBot)
[![Release](https://img.shields.io/github/release/toby3d/MyPackBot.svg)](https://github.com/toby3d/MyPackBot/releases/latest)
[![Patreon](https://img.shields.io/badge/support-patreon-E6461A.svg?maxAge=2592000)](https://www.patreon.com/toby3d)
![bot logo](https://raw.githubusercontent.com/toby3d/MyPackBot/gh-pages/static/social/og-image.jpg)
## Wat?
This is a Telegram-bot that collects all the stickers sent to it in one (almost) infinite pack. No more, no less.
**Benefits:**
- Does not require creation of a set with a unique URL and/or name;
- Indeed (almost) unlimited pack size;
- Keeps stickers belonging to their original sets;
- Fully support the standard functionality of Telegram stickers (for example "add to favorites");
- Avaliable anywhere in Telegram by typing `@MyPackBot ` in the input field;
- Supports filtering of results by emoji's: `@MyPackBot 😀👍`;
- Fast as f\*\*\*king Sonic;
- Worked with uploadable WebP stickers;
- Worked with blocked by rightholders sets (but this is not exact);
**Disadvantages:**
- Requires type `@MyPackBot ` in the input field;
- Availability depends on the internet connection and bot uptime;
- Supports search/filtering only for first emoji associated with sticker;
- Does not support synchronization of the updated original set contents with the saved set contents in the bot.
## Why?
Because Telegram native tools for managing stickers are somewhat limited:
- User can have only **up to 200 active stickers sets**;
- In one set can be uploaded **up to 120 stickers**;
- User can have only **up to 5 favorites stickers**;
Having done simple mathematical calculations, we can assume that the **maximum user capacity** (when he has the maximum number of sets, each of which contains the maximum number of stickers) **is equal 24,000 stickers**.
But, as usual, there are problems:
- **Most of the sets are incomplete** and contain less than 120 stickers (sometimes - only 1-3 stickers on whole set);
- **Some sets contains junk, duplicated and promotional stickers**;
- **Sometimes user want use own stickers** by uploading WebP files, but without creating new sticker set;
- Anyway, **user just want have as many stickers as he want**;
To solve these problems, this bot was designed.
## How?
### tl;dr
- Telegram API [supports stickers as results](https://core.telegram.org/bots/api#inlinequeryresultcachedsticker) in inline query;
- Telegram API allows to use someone else's FileID for results;
- It is not necessary to [create a new set](https://core.telegram.org/bots/api#createnewstickerset) using Telegram, since it only "references" existing files;
- Bot saves only [user](https://core.telegram.org/bots/api#user) info, [sticker and name of his set](https://core.telegram.org/bots/api#sticker) in the database if user upload custom sticker or send/forward already existing;
- Database architecture allows to filter keys by user ID and sort them by set name and emoji value;
- When requesting inline query, bot simply create results from filtered database keys;
- ???????
- PROFIT!!1
## Step-by-step
I'm too lazy to write, so just check the source code for the comments. 👀
### Dependencies
Bot uses the following dependencies:
- Written on [Go](https://github.com/golang/go) language, because I <3 Go;
- I ventured to migrate to my own [telegram](https://github.com/toby3d/telegram) package to win in convenience and productivity;
- I use [dlog](https://github.com/kirillDanshin/dlog) for debugging without spamming on production server by use only one build flag;
- Data of users and stickers save thanks to [BuntDB](https://github.com/tidwall/buntdb);
## Support
### GitHub
You can [request fix/add some things](https://github.com/toby3d/MyPackBot/issues/new), [make a patch](https://github.com/toby3d/MyPackBot/compare) or help with [translation and localization](https://github.com/toby3d/MyPackBot/tree/develop/translations) on your language.
Ah, and star this repo, of course.
### Patreon
**I work on my own projects in my free time.** Please think about the [financial support for my independence](https://patreon.com/toby3d) so that I can devote more time to this bot and other projects. In exchange for an award!
### Social
Subscribe, follow my resources and feel free to maintain contact with me: https://toby3d.github.io

9
docs/SUPPORT.md Normal file
View File

@ -0,0 +1,9 @@
# [Support me on Patreon!](https://www.patreon.com/bePatron?c=243288)
I develop this project in my spare time, and I do it and I will do it free of charge. However, you can make a donation or become a sponsor to make sure that I have enough coffee and pizza for night coding.
**These people sponsored current version of the project:**
- Aurielb
- Daniil Tlenov
- @kirillDanshin
- MoD21k
- @YamiOdymel

View File

@ -1,8 +0,0 @@
package main
// errCheck helps debug critical errors without warnings from 'gocyclo' linter
func errCheck(err error) {
if err != nil {
panic(err.Error())
}
}

View File

@ -1,59 +0,0 @@
package main
import (
"fmt"
log "github.com/kirillDanshin/dlog" // Insert logs only in debug builds
"github.com/toby3d/go-telegram" // My Telegram bindings
)
// allowedUpdates is a value for parameter of updates configuration
var allowedUpdates = []string{
telegram.UpdateChosenInlineResult, // For collecting statistics
telegram.UpdateInlineQuery, // For searching and sending stickers
telegram.UpdateMessage, // For get commands and messages
}
// getUpdatesChannel return webhook or long polling channel with bot updates
func getUpdatesChannel() telegram.UpdatesChannel {
log.Ln("Preparing channel for updates...")
if !*flagWebhook {
log.Ln("Use LongPolling updates")
log.Ln("Deleting webhook if exists")
_, err := bot.DeleteWebhook()
errCheck(err)
return bot.NewLongPollingChannel(&telegram.GetUpdatesParameters{
Offset: 0,
Limit: 100,
Timeout: 60,
AllowedUpdates: allowedUpdates,
})
}
set := cfg.UString("telegram.webhook.set")
listen := cfg.UString("telegram.webhook.listen")
serve := cfg.UString("telegram.webhook.serve")
log.Ln(
"Trying set webhook on address:",
fmt.Sprint(set, listen, bot.AccessToken),
)
log.Ln("Creating new webhook...")
webhook := telegram.NewWebhook(
fmt.Sprint(set, listen, bot.AccessToken), nil,
)
webhook.MaxConnections = 100
webhook.AllowedUpdates = allowedUpdates
return bot.NewWebhookChannel(
webhook, // params
"", // certFile
"", // keyFile
set, // set
fmt.Sprint(listen, bot.AccessToken), // listen
serve, // serve
)
}

View File

@ -0,0 +1,4 @@
#!/bin/sh
# gitmoji as a commit hook
exec < /dev/tty
gitmoji --hook $1

38
help.go
View File

@ -1,38 +0,0 @@
package main
import "github.com/toby3d/go-telegram" // My Telegram bindings
func commandHelp(msg *telegram.Message) {
bot.SendChatAction(msg.Chat.ID, telegram.ActionTyping)
err := dbChangeUserState(msg.From.ID, stateNone)
errCheck(err)
T, err := switchLocale(msg.From.LanguageCode)
errCheck(err)
markup := telegram.NewInlineKeyboardMarkup(
telegram.NewInlineKeyboardRow(
telegram.NewInlineKeyboardButtonSwitch(
T("button_share"),
" ",
),
),
)
reply := telegram.NewMessage(
msg.Chat.ID, T("reply_help", map[string]interface{}{
"AddStickerCommand": cmdAddSticker,
"AddPackCommand": cmdAddPack,
"DeleteCommand": cmdDelete,
"ResetCommand": cmdReset,
"CancelCommand": cmdCancel,
"Username": bot.Self.Username,
}),
)
reply.ParseMode = telegram.ModeMarkdown
reply.ReplyMarkup = &markup
_, err = bot.SendMessage(reply)
errCheck(err)
}

View File

@ -1,11 +0,0 @@
button_inline_empty:
other: Your pack is empty
button_inline_nothing:
other: Not found stickers for {{.Query}}, add one?
button_inline_add:
one: You have {{.Count}} sticker. Add one more?
other: You have {{.Count}} stickers. Add one more?
button_remove:
other: Select sticker for remove
button_share:
other: Use your stickers pack!

View File

@ -1,10 +0,0 @@
cancel_add_sticker:
other: You cancelled the process of adding a new sticker to your pack.
cancel_add_pack:
other: You cancelled the process of adding a new pack to yours.
cancel_del:
other: You cancelled the process of removing a sticker from your pack.
cancel_reset:
other: You cancelled the process of reseting your stickers pack.
cancel_error:
other: Nothing to cancel.

View File

@ -1,6 +0,0 @@
meta_reset_1:
other: Wait. Who are you?
meta_reset_2:
other: What are you doing here?
meta_reset_3:
other: What am I doing here?

View File

@ -1,37 +1,45 @@
button_inline_add:
one: You have {{.Count}} sticker. Add one more?
other: You have {{.Count}} stickers. Add one more?
button_add_pack:
other: "\U0001F4E6 Add pack"
button_add_sticker:
other: Add sticker
button_cancel:
other: ❌ Cancel
button_del_pack:
other: "\U0001F5D1 Delete pack"
button_del_sticker:
other: "\U0001F5D1 Delete sticker"
button_inline_empty:
other: Your pack is empty
button_inline_nothing:
other: Not found stickers for {{.Query}}, add one?
button_remove:
other: Select sticker for remove
other: Not found stickers for {{.Query}}
button_inline_search:
one: You have {{.Count}} sticker
other: You have {{.Count}} stickers
button_inline_select:
other: Select sticker
button_reset:
other: "\U0001F525 Reset pack"
button_share:
other: Use your stickers pack!
cancel_add:
other: You cancelled the process of adding a new sticker to your pack.
cancel_add_pack:
other: You cancelled the process of adding a new pack to yours.
other: You cancelled the process of adding a new packs to yours.
cancel_add_sticker:
other: You cancelled the process of adding a new sticker to your pack.
cancel_del:
other: You cancelled the process of removing a sticker from your pack.
other: You cancelled the process of adding a new stickers to your pack.
cancel_del_pack:
other: You cancelled the process of removing a sticker packs from yours.
cancel_del_sticker:
other: You cancelled the process of removing a stickers from your pack.
cancel_error:
other: Nothing to cancel.
cancel_remove:
other: You cancelled the process of removing a sticker from your pack.
cancel_reset:
other: You cancelled the process of reseting your stickers pack.
error_already_add:
other: This sticker is already in your pack.
error_already_add_pack:
other: All stickers from *{{.SetTitle}}* pack is already in yours.
error_already_add_sticker:
other: This sticker is already in your pack.
error_already_del:
other: Maybe this sticker is already removed from your pack.
error_already_remove:
error_already_del_pack:
other: Maybe this pack is already removed from yours.
error_already_del_sticker:
other: Maybe this sticker is already removed from your pack.
error_already_reset:
other: There is nothing to reset, pack is already empty.
@ -40,40 +48,33 @@ error_empty_add_pack:
instead.
error_empty_del:
other: There is nothing to remove, pack is empty.
error_empty_remove:
other: There is nothing to remove, pack is empty.
error_reset_phrase:
other: Invalid phrase of resetting. This action has been canceled.
error_unknown:
other: |-
I do not know what to do with this sticker.
Please run /{{.AddStickerCommand}}, /{{.AddPackCommand}} or /{{.DeleteCommand}} command first.
meta_reset_1:
other: Wait. Who are you?
meta_reset_2:
other: What are you doing here?
meta_reset_3:
other: What am I doing here?
reply_add:
other: Send an existing sticker from any other pack to add it to yourself.
Please run /{{.AddStickerCommand}}, /{{.AddPackCommand}}, /{{.DeleteStickerCommand}} or /{{.DeletePackCommand}} command first.
key_phrase:
other: Yes, I am totally sure.
reply_add_pack:
other: Send an existing stickers from any other packs to add the entire packs to
yourself.
reply_add_sticker:
other: Send an existing stickers from any other packs to add it to yourself.
reply_del:
reply_del_pack:
other: Send an existing stickers from your pack to delete its entire set.
reply_del_sticker:
other: Send an existing stickers from your pack for removing it.
reply_help:
other: |-
other: |
/{{.AddStickerCommand}} - add a single sticker to your pack
/{{.AddPackCommand}} - add a full other pack to your pack
/{{.DeleteCommand}} - remove a single sticker from your pack
/{{.DeleteStickerCommand}} - remove a single sticker from your pack
/{{.DeletePackCommand}} - remove a sticker set from your pack
/{{.ResetCommand}} - remove all stickers from your pack
/{{.CancelCommand}} - cancel the current operation
To view and send stickers from your pack, just type `@{{.Username}}` (and space) in any chat.
reply_remove:
other: Send an existing sticker from your pack for removing it.
reply_reset:
other: |
This operation will remove *all* stickers from your pack and *this can't be undone*.
@ -85,15 +86,16 @@ reply_start:
Hello, I'm the [@{{.Username}}](tg://user?id={{.ID}})!
I can create your personal pack with stickers from others packs.
Without limits and installing. In any chat. For free.
success_add:
other: The sticker was successfully added to your pack!
reply_switch_button:
other: This button will help you quickly call your pack to select the sticker you
want.
success_add_pack:
other: The sticker pack *{{.SetTitle}}* was successfully added to yours!
success_add_sticker:
other: The sticker was successfully added to your pack!
success_del:
other: The sticker was successfully removed from your pack!
success_remove:
success_del_pack:
other: The sticker pack *{{.SetTitle}}* was successfully removed from yours!
success_del_sticker:
other: The sticker was successfully removed from your pack!
success_reset:
other: The contents of your pack are completely reset!..

101
i18n/ru.all.yaml Normal file
View File

@ -0,0 +1,101 @@
button_add_pack:
other: "\U0001F4E6 Добавить набор"
button_add_sticker:
other: Добавить стикер
button_cancel:
other: ❌ Отменить
button_del_pack:
other: "\U0001F5D1 Удалить набор"
button_del_sticker:
other: "\U0001F5D1 Удалить стикер"
button_inline_empty:
other: Твой набор пуст
button_inline_nothing:
other: Не найдены стикеры для {{.Query}}
button_inline_search:
few: У тебя {{.Count}} стикера
many: У тебя {{.Count}} стикеров
one: У тебя {{.Count}} стикер
other: У тебя {{.Count}} стикеров
button_inline_select:
other: Выбрать стикер
button_reset:
other: "\U0001F525 Сбросить набор"
button_share:
other: Воспользоваться твоим набором!
cancel_add_pack:
other: Ты отменил процесс добавления новых наборов в твой.
cancel_add_sticker:
other: Ты отменил процесс добавления новых стикеров в твой набор.
cancel_del_pack:
other: Ты отменил процесс удаления наборов из твоего набора.
cancel_del_sticker:
other: Ты отменил процесс удаления стикера из твоего набора.
cancel_error:
other: Нечего отменять.
cancel_reset:
other: Ты отменил процесс сброса твоего набора.
error_already_add_pack:
other: Все стикеры *{{.SetTitle}}* уже в твоём наборе.
error_already_add_sticker:
other: Этот стикер уже в твоём наборе.
error_already_del_pack:
other: Вероятно этот набор уже удалён из твоего.
error_already_del_sticker:
other: Вероятно этот стикер уже удалён из твоего набора.
error_already_reset:
other: Нечего сбрасывать, набор уже пуст.
error_empty_add_pack:
other: Кажется ты пытаешься добавить собственный стикер. Используй для этого /{{.AddStickerCommand}}.
error_empty_del:
other: Нечего удалять, набор уже пуст.
error_reset_phrase:
other: Неправильная фраза для сброса. Действие было отменено.
error_unknown:
other: |-
Я понятия не имею что делать с этим стикером.
Пожалуйста, сначала примени /{{.AddStickerCommand}}, /{{.AddPackCommand}}, /{{.DeleteStickerCommand}} или /{{.DeletePackCommand}}.
key_phrase:
other: Да, я абсолютно уверен.
reply_add_pack:
other: Пришли стикеры из любых других наборов чтобы целиком добавить их наборы в
свой.
reply_add_sticker:
other: Пришли стикеры из любых других наборов чтобы по-одному добавить их в свой.
reply_del_pack:
other: Пришли стикер из своего набора чтобы удалить весь его набор.
reply_del_sticker:
other: Пришли стикер из своего набора чтобы удалить его.
reply_help:
other: |
/{{.AddStickerCommand}} - по-одному добавляет стикеры в твой набор
/{{.AddPackCommand}} - добавляет сразу весь набор в твой
/{{.DeleteStickerCommand}} - по-одному удаляет стикер из твоего набора
/{{.DeletePackCommand}} - удаляет набор стикеров из твоего набора
/{{.ResetCommand}} - удаляет все стикеры из твоего набора
/{{.CancelCommand}} - отменяет текущую операцию
Для просмотра и отправки стикеров из твоего набора просто набери `@{{.Username}}` (и пробел) в любом чате.
reply_reset:
other: |
Эта операция удалит *все* стикеры из твоего набора и *это не может быть отменено*.
Напиши `{{.KeyPhrase}}` чтобы подтвердить своё намерение обнулить мои мозги (о боже зачем).
Или используй /{{.CancelCommand}} чтобы отменить текущую операцию.
reply_start:
other: |
Привет, я [@{{.Username}}](tg://user?id={{.ID}})!
Я могу создать твой персональный набор стикеров из других наборов.
Без ограничений и установки. В любых чатах. Бесплатно.
reply_switch_button:
other: Эта кнопка поможет тебе быстро вызвать твой набор для выбора нужного стикера.
success_add_pack:
other: Набор *{{.SetTitle}}* успешно добавлен в твой!
success_add_sticker:
other: Стикер успешно добавлен в твой набор!
success_del_pack:
other: Набор *{{.SetTitle}}* успешно удалён из твоего набора!
success_del_sticker:
other: Стикер успешно удалён из твоего набора!
success_reset:
other: Сброс твоего набора успешно произведён!..

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,23 @@
button_inline_empty:
other: Your pack is empty
button_inline_nothing:
other: Not found stickers for {{.Query}}
button_inline_search:
one: You have {{.Count}} sticker
other: You have {{.Count}} stickers
button_inline_select:
other: Select sticker
button_share:
other: Use your stickers pack!
button_add_sticker:
other: Add sticker
button_add_pack:
other: 📦 Add set
button_del_sticker:
other: 🗑 Delete sticker
button_del_pack:
other: 🗑 Delete set
button_reset:
other: 🔥 Reset pack
button_cancel:
other: ❌ Cancel

12
i18n/src/en/en.cancel.yaml Executable file
View File

@ -0,0 +1,12 @@
cancel_add_sticker:
other: You cancelled the process of adding a new stickers to your pack.
cancel_add_pack:
other: You cancelled the process of adding a new sets to your pack.
cancel_del_sticker:
other: You cancelled the process of removing a stickers from your pack.
cancel_del_pack:
other: You cancelled the process of removing a sets from your pack.
cancel_reset:
other: You cancelled the process of reseting your stickers pack.
cancel_error:
other: Nothing to cancel.

View File

@ -1,18 +1,20 @@
error_already_add_sticker:
other: This sticker is already in your pack.
error_already_add_pack:
other: All stickers from *{{.SetTitle}}* pack is already in yours.
error_already_del:
other: All stickers from *{{.SetTitle}}* set is already in yours.
error_already_del_sticker:
other: Maybe this sticker is already removed from your pack.
error_already_del_pack:
other: Maybe this set is already removed from your pack.
error_already_reset:
other: There is nothing to reset, pack is already empty.
error_reset_phrase:
other: Invalid phrase of resetting. This action has been canceled.
error_empty_del:
other: There is nothing to remove, pack is empty.
other: There is nothing to remove, your pack is empty.
error_empty_add_pack:
other: You try to add a sticker that exists outside the packs. Use /{{.AddStickerCommand}} instead.
error_unknown:
other: |
I do not know what to do with this sticker.
Please run /{{.AddStickerCommand}}, /{{.AddPackCommand}} or /{{.DeleteCommand}} command first.
Please run /{{.AddStickerCommand}}, /{{.AddPackCommand}}, /{{.DeleteStickerCommand}} or /{{.DeletePackCommand}} command first.

19
i18n/en-us.replies.yaml → i18n/src/en/en.replies.yaml Normal file → Executable file
View File

@ -1,14 +1,16 @@
reply_start:
other: |
Hello, I'm the [@{{.Username}}](tg://user?id={{.ID}})!
I can create your personal pack with stickers from others packs.
I can create your personal stickers pack with stickers from others sets.
Without limits and installing. In any chat. For free.
reply_add_sticker:
other: Send an existing stickers from any other packs to add it to yourself.
other: Send an existing stickers from any other sets to add it to your pack.
reply_add_pack:
other: Send an existing stickers from any other packs to add the entire packs to yourself.
reply_del:
other: Send an existing stickers from any other sets to add the entire sets to your pack.
reply_del_sticker:
other: Send an existing stickers from your pack for removing it.
reply_del_pack:
other: Send an existing stickers from your pack to delete its entire set.
reply_reset:
other: |
This operation will remove *all* stickers from your pack and *this can't be undone*.
@ -18,9 +20,12 @@ reply_reset:
reply_help:
other: |
/{{.AddStickerCommand}} - add a single sticker to your pack
/{{.AddPackCommand}} - add a full other pack to your pack
/{{.DeleteCommand}} - remove a single sticker from your pack
/{{.AddPackCommand}} - add a full stickers set to your pack
/{{.DeleteStickerCommand}} - remove a single sticker from your pack
/{{.DeletePackCommand}} - remove a sticker set from your pack
/{{.ResetCommand}} - remove all stickers from your pack
/{{.CancelCommand}} - cancel the current operation
To view and send stickers from your pack, just type `@{{.Username}}` (and space) in any chat.
To view and send stickers from your pack, just type `@{{.Username}}` (and space) in any chat.
reply_switch_button:
other: This button will help you quickly call your pack to select the sticker you want.

View File

@ -2,7 +2,9 @@ success_add_sticker:
other: The sticker was successfully added to your pack!
success_add_pack:
other: The sticker pack *{{.SetTitle}}* was successfully added to yours!
success_del:
success_del_sticker:
other: The sticker was successfully removed from your pack!
success_del_pack:
other: The sticker pack *{{.SetTitle}}* was successfully removed from yours!
success_reset:
other: The contents of your pack are completely reset!..
other: The contents of your pack are completely reset!

2
i18n/src/en/en.yaml Normal file
View File

@ -0,0 +1,2 @@
key_phrase:
other: Yes, I am totally sure.

View File

@ -0,0 +1,25 @@
button_inline_empty:
other: Твой набор пуст
button_inline_nothing:
other: Не найдены стикеры для {{.Query}}
button_inline_search:
few: У тебя {{.Count}} стикера
one: У тебя {{.Count}} стикер
many: У тебя {{.Count}} стикеров
other: У тебя {{.Count}} стикеров
button_inline_select:
other: Выбрать стикер
button_share:
other: Воспользоваться твоим набором!
button_add_sticker:
other: Добавить стикер
button_add_pack:
other: 📦 Добавить набор
button_del_sticker:
other: 🗑 Удалить стикер
button_del_pack:
other: 🗑 Удалить набор
button_reset:
other: 🔥 Сбросить набор
button_cancel:
other: ❌ Отменить

12
i18n/src/ru/ru.cancel.yaml Executable file
View File

@ -0,0 +1,12 @@
cancel_add_sticker:
other: Ты отменил процесс добавления новых стикеров в твой набор.
cancel_add_pack:
other: Ты отменил процесс добавления новых наборов в твой.
cancel_del_sticker:
other: Ты отменил процесс удаления стикера из твоего набора.
cancel_del_pack:
other: Ты отменил процесс удаления наборов из твоего набора.
cancel_reset:
other: Ты отменил процесс сброса твоего набора.
cancel_error:
other: Нечего отменять.

20
i18n/src/ru/ru.errors.yaml Executable file
View File

@ -0,0 +1,20 @@
error_already_add_sticker:
other: Этот стикер уже в твоём наборе.
error_already_add_pack:
other: Все стикеры *{{.SetTitle}}* уже в твоём наборе.
error_already_del_sticker:
other: Вероятно этот стикер уже удалён из твоего набора.
error_already_del_pack:
other: Вероятно этот набор уже удалён из твоего.
error_already_reset:
other: Нечего сбрасывать, набор уже пуст.
error_reset_phrase:
other: Неправильная фраза для сброса. Действие было отменено.
error_empty_del:
other: Нечего удалять, набор уже пуст.
error_empty_add_pack:
other: Кажется ты пытаешься добавить собственный стикер. Используй для этого /{{.AddStickerCommand}}.
error_unknown:
other: |
Я понятия не имею что делать с этим стикером.
Пожалуйста, сначала примени /{{.AddStickerCommand}}, /{{.AddPackCommand}}, /{{.DeleteStickerCommand}} или /{{.DeletePackCommand}}.

31
i18n/src/ru/ru.replies.yaml Executable file
View File

@ -0,0 +1,31 @@
reply_start:
other: |
Привет, я [@{{.Username}}](tg://user?id={{.ID}})!
Я могу создать твой персональный набор стикеров из других наборов.
Без ограничений и установки. В любых чатах. Бесплатно.
reply_add_sticker:
other: Пришли стикеры из любых других наборов чтобы по-одному добавить их в свой.
reply_add_pack:
other: Пришли стикеры из любых других наборов чтобы целиком добавить их наборы в свой.
reply_del_sticker:
other: Пришли стикер из своего набора чтобы удалить его.
reply_del_pack:
other: Пришли стикер из своего набора чтобы удалить весь его набор.
reply_reset:
other: |
Эта операция удалит *все* стикеры из твоего набора и *это не может быть отменено*.
Напиши `{{.KeyPhrase}}` чтобы подтвердить своё намерение обнулить мои мозги (о боже зачем).
Или используй /{{.CancelCommand}} чтобы отменить текущую операцию.
reply_help:
other: |
/{{.AddStickerCommand}} - по-одному добавляет стикеры в твой набор
/{{.AddPackCommand}} - добавляет сразу весь набор в твой
/{{.DeleteStickerCommand}} - по-одному удаляет стикер из твоего набора
/{{.DeletePackCommand}} - удаляет набор стикеров из твоего набора
/{{.ResetCommand}} - удаляет все стикеры из твоего набора
/{{.CancelCommand}} - отменяет текущую операцию
Для просмотра и отправки стикеров из твоего набора просто набери `@{{.Username}}` (и пробел) в любом чате.
reply_switch_button:
other: Эта кнопка поможет тебе быстро вызвать твой набор для выбора нужного стикера.

10
i18n/src/ru/ru.success.yaml Executable file
View File

@ -0,0 +1,10 @@
success_add_sticker:
other: Стикер успешно добавлен в твой набор!
success_add_pack:
other: Набор *{{.SetTitle}}* успешно добавлен в твой!
success_del_sticker:
other: Стикер успешно удалён из твоего набора!
success_del_pack:
other: Набор *{{.SetTitle}}* успешно удалён из твоего набора!
success_reset:
other: Сброс твоего набора успешно произведён!

2
i18n/src/ru/ru.yaml Normal file
View File

@ -0,0 +1,2 @@
key_phrase:
other: Да, я абсолютно уверен.

68
init.go
View File

@ -1,68 +0,0 @@
package main
import (
"flag"
"os"
"path/filepath"
"strings"
log "github.com/kirillDanshin/dlog" // Insert logs only in debug builds
"github.com/nicksnyder/go-i18n/i18n" // Internationalization and localization
"github.com/olebedev/config" // Easy configuration file parsing
"github.com/toby3d/botan"
)
var (
// Variables with types from imports
cfg *config.Config
appMetrika *botan.Botan
// Setted variables
metrika = make(chan bool)
flagWebhook = flag.Bool(
"webhook",
false,
"enable work via webhooks (required valid certificates)",
)
)
// init prepare configuration and other things for successful start of main
// function.
func init() {
log.Ln("Initializing...")
log.Ln("Parse flags...")
flag.Parse()
err := filepath.Walk("./i18n/", func(path string, info os.FileInfo, err error) error {
if strings.HasSuffix(path, ".all.yaml") {
i18n.MustLoadTranslationFile(path)
}
return nil
})
errCheck(err)
log.Ln("Loading configuration file...")
cfg, err = config.ParseYamlFile("config.yaml")
errCheck(err)
appMetrika = botan.New(cfg.UString("botan"))
log.Ln("Checking bot access token in configuration file...")
_, err = cfg.String("telegram.token")
errCheck(err)
if *flagWebhook {
log.Ln("Enabled webhook mode, check configuration strings...")
log.Ln("Checking webhook set string...")
_, err = cfg.String("telegram.webhook.set")
errCheck(err)
log.Ln("Checking webhook listen string...")
_, err = cfg.String("telegram.webhook.listen")
errCheck(err)
log.Ln("Checking webhook listen string...")
_, err = cfg.String("telegram.webhook.serve")
errCheck(err)
}
}

View File

@ -1,132 +0,0 @@
package main
import (
"strconv"
"strings"
log "github.com/kirillDanshin/dlog" // Insert logs only in debug builds
"github.com/toby3d/go-telegram" // My Telegram bindings
)
const perPage = 50
var r = strings.NewReplacer(
"🏻", "",
"🏼", "",
"🏽", "",
"🏾", "",
"🏿", "",
)
func inlineQuery(inline *telegram.InlineQuery) {
inline.Query = r.Replace(inline.Query)
log.Ln("Let's preparing answer...")
T, err := switchLocale(inline.From.LanguageCode)
errCheck(err)
log.Ln("INLINE OFFSET:", inline.Offset)
if inline.Offset == "" {
inline.Offset = "-1"
}
offset, err := strconv.Atoi(inline.Offset)
errCheck(err)
if offset == -1 {
offset++
}
answer := &telegram.AnswerInlineQueryParameters{
InlineQueryID: inline.ID,
CacheTime: 1,
IsPersonal: true,
}
stickers, emojis, err := dbGetUserStickers(inline.From.ID)
errCheck(err)
packSize := len(stickers)
if inline.Query != "" {
var buffer []string
for i := range stickers {
if emojis[i] != inline.Query {
continue
}
buffer = append(buffer, stickers[i])
}
stickers = buffer
}
totalStickers := len(stickers)
totalPages := totalStickers / perPage
if totalStickers == 0 {
if inline.Query != "" {
// If search stickers by emoji return 0 results
answer.SwitchPrivateMessageText = T(
"button_inline_nothing",
map[string]interface{}{"Query": inline.Query},
)
answer.SwitchPrivateMessageParameter = cmdAddSticker
} else {
// If query is empty and get 0 stickers
answer.SwitchPrivateMessageText = T("button_inline_empty")
answer.SwitchPrivateMessageParameter = cmdAddSticker
}
} else {
log.Ln("LESS THAN:", offset < totalPages)
if offset < totalPages {
from := offset * perPage
if offset > 0 {
from--
}
to := from + perPage
log.Ln("from:", from)
log.Ln("to:", to)
stickers = stickers[from:to]
offset++
answer.NextOffset = strconv.Itoa(offset)
} else {
from := offset * perPage
if offset > 0 {
from--
}
to := from
log.Ln("MINUS:", totalStickers%perPage)
if totalStickers%perPage != 0 {
log.Ln("FUCK")
to += totalStickers % perPage
} else {
to += perPage
}
log.Ln("from:", from)
log.Ln("to:", to)
stickers = stickers[from:to]
}
log.Ln("Stickers after checks:", len(stickers))
var results = make([]interface{}, len(stickers))
for i, sticker := range stickers {
results[i] = telegram.NewInlineQueryResultCachedSticker(
sticker, sticker,
)
}
answer.SwitchPrivateMessageText = T(
"button_inline_add",
packSize,
map[string]interface{}{
"Count": packSize,
})
answer.SwitchPrivateMessageParameter = cmdAddSticker
answer.Results = results
}
_, err = bot.AnswerInlineQuery(answer)
errCheck(err)
}

35
internal/actions/action.go Executable file
View File

@ -0,0 +1,35 @@
package actions
import (
log "github.com/kirillDanshin/dlog"
"gitlab.com/toby3d/mypackbot/internal/db"
"gitlab.com/toby3d/mypackbot/internal/errors"
"gitlab.com/toby3d/mypackbot/internal/models"
tg "gitlab.com/toby3d/telegram"
)
// Action function check Message update on commands, sended stickers or other
// user stuff if user state is not 'none'
func Action(msg *tg.Message) {
state, err := db.DB.GetUserState(msg.From)
errors.Check(err)
log.Ln("state:", state)
switch state {
case models.StateAddSticker:
Add(msg, false)
case models.StateAddPack:
Add(msg, true)
case models.StateDeleteSticker:
Delete(msg, false)
case models.StateDeletePack:
Delete(msg, true)
case models.StateReset:
Reset(msg)
default:
err = db.DB.ChangeUserState(msg.From, models.StateNone)
errors.Check(err)
Error(msg)
}
}

81
internal/actions/add.go Normal file
View File

@ -0,0 +1,81 @@
package actions
import (
log "github.com/kirillDanshin/dlog"
"gitlab.com/toby3d/mypackbot/internal/bot"
"gitlab.com/toby3d/mypackbot/internal/db"
"gitlab.com/toby3d/mypackbot/internal/errors"
"gitlab.com/toby3d/mypackbot/internal/i18n"
"gitlab.com/toby3d/mypackbot/internal/models"
"gitlab.com/toby3d/mypackbot/internal/utils"
tg "gitlab.com/toby3d/telegram"
)
// Add action add sticker or set to user's pack
func Add(msg *tg.Message, pack bool) {
if !msg.IsSticker() {
return
}
t, err := i18n.SwitchTo(msg.From.LanguageCode)
errors.Check(err)
_, err = bot.Bot.SendChatAction(msg.Chat.ID, tg.ActionTyping)
errors.Check(err)
reply := tg.NewMessage(msg.Chat.ID, t("success_add_sticker"))
reply.ParseMode = tg.StyleMarkdown
if !pack {
var exist bool
sticker := msg.Sticker
exist, err = db.DB.AddSticker(msg.From, sticker)
errors.Check(err)
if exist {
reply.Text = t("error_already_add_sticker")
}
reply.ReplyMarkup = utils.CancelButton(t)
_, err = bot.Bot.SendMessage(reply)
errors.Check(err)
return
}
reply.Text = t("error_empty_add_pack", map[string]interface{}{
"AddStickerCommand": models.CommandAddSticker,
})
if msg.Sticker.SetName != "" {
var set *tg.StickerSet
set, err = bot.Bot.GetStickerSet(msg.Sticker.SetName)
errors.Check(err)
log.Ln("SetTitle:", set.Title)
reply.Text = t("success_add_pack", map[string]interface{}{
"SetTitle": set.Title,
})
allExists := true
for i := range set.Stickers {
var exist bool
exist, err = db.DB.AddSticker(msg.From, &set.Stickers[i])
errors.Check(err)
if !exist {
allExists = false
}
}
log.Ln("All exists?", allExists)
if allExists {
reply.Text = t("error_already_add_pack", map[string]interface{}{
"SetTitle": set.Title,
})
}
}
reply.ReplyMarkup = utils.CancelButton(t)
_, err = bot.Bot.SendMessage(reply)
errors.Check(err)
}

View File

@ -0,0 +1,54 @@
package actions
import (
log "github.com/kirillDanshin/dlog"
"gitlab.com/toby3d/mypackbot/internal/bot"
"gitlab.com/toby3d/mypackbot/internal/db"
"gitlab.com/toby3d/mypackbot/internal/errors"
"gitlab.com/toby3d/mypackbot/internal/i18n"
"gitlab.com/toby3d/mypackbot/internal/utils"
tg "gitlab.com/toby3d/telegram"
)
// Delete action remove sticker or set from user's pack
func Delete(msg *tg.Message, pack bool) {
if !msg.IsSticker() {
return
}
t, err := i18n.SwitchTo(msg.From.LanguageCode)
errors.Check(err)
_, err = bot.Bot.SendChatAction(msg.Chat.ID, tg.ActionTyping)
errors.Check(err)
reply := tg.NewMessage(msg.Chat.ID, t("success_del_sticker"))
reply.ParseMode = tg.StyleMarkdown
reply.ReplyMarkup = utils.CancelButton(t)
var notExist bool
if pack {
var set *tg.StickerSet
set, err = bot.Bot.GetStickerSet(msg.Sticker.SetName)
errors.Check(err)
log.Ln("SetName:", set.Title)
reply.Text = t("success_del_pack", map[string]interface{}{
"SetTitle": set.Title,
})
notExist, err = db.DB.DeletePack(msg.From, msg.Sticker)
if notExist {
reply.Text = t("error_already_del_pack")
}
} else {
notExist, err = db.DB.DeleteSticker(msg.From, msg.Sticker)
if notExist {
reply.Text = t("error_already_del_sticker")
}
}
errors.Check(err)
_, err = bot.Bot.SendMessage(reply)
errors.Check(err)
}

33
internal/actions/error.go Normal file
View File

@ -0,0 +1,33 @@
package actions
import (
"gitlab.com/toby3d/mypackbot/internal/bot"
"gitlab.com/toby3d/mypackbot/internal/errors"
"gitlab.com/toby3d/mypackbot/internal/i18n"
"gitlab.com/toby3d/mypackbot/internal/models"
"gitlab.com/toby3d/mypackbot/internal/utils"
tg "gitlab.com/toby3d/telegram"
)
// Error action send error reply about invalid user request
func Error(msg *tg.Message) {
t, err := i18n.SwitchTo(msg.From.LanguageCode)
errors.Check(err)
_, err = bot.Bot.SendChatAction(msg.Chat.ID, tg.ActionTyping)
errors.Check(err)
reply := tg.NewMessage(
msg.Chat.ID, t("error_unknown", map[string]interface{}{
"AddStickerCommand": models.CommandAddSticker,
"AddPackCommand": models.CommandAddPack,
"DeleteStickerCommand": models.CommandDeleteSticker,
"DeletePackCommand": models.CommandDeletePack,
}),
)
reply.ParseMode = tg.StyleMarkdown
reply.ReplyMarkup = utils.MenuKeyboard(t)
_, err = bot.Bot.SendMessage(reply)
errors.Check(err)
}

44
internal/actions/reset.go Normal file
View File

@ -0,0 +1,44 @@
package actions
import (
"strings"
"gitlab.com/toby3d/mypackbot/internal/bot"
"gitlab.com/toby3d/mypackbot/internal/db"
"gitlab.com/toby3d/mypackbot/internal/errors"
"gitlab.com/toby3d/mypackbot/internal/i18n"
"gitlab.com/toby3d/mypackbot/internal/models"
"gitlab.com/toby3d/mypackbot/internal/utils"
tg "gitlab.com/toby3d/telegram"
)
// Reset action checks key phrase and reset user's pack
func Reset(msg *tg.Message) {
t, err := i18n.SwitchTo(msg.From.LanguageCode)
errors.Check(err)
err = db.DB.ChangeUserState(msg.From, models.StateNone)
errors.Check(err)
_, err = bot.Bot.SendChatAction(msg.Chat.ID, tg.ActionTyping)
errors.Check(err)
if !strings.EqualFold(msg.Text, t("key_phrase")) {
reply := tg.NewMessage(msg.Chat.ID, t("error_reset_phrase"))
reply.ParseMode = tg.StyleMarkdown
reply.ReplyMarkup = utils.MenuKeyboard(t)
_, err = bot.Bot.SendMessage(reply)
errors.Check(err)
return
}
err = db.DB.ResetUser(msg.From)
errors.Check(err)
reply := tg.NewMessage(msg.Chat.ID, t("success_reset"))
reply.ParseMode = tg.StyleMarkdown
reply.ReplyMarkup = utils.MenuKeyboard(t)
_, err = bot.Bot.SendMessage(reply)
errors.Check(err)
}

11
internal/bot/new.go Normal file
View File

@ -0,0 +1,11 @@
package bot
import tg "gitlab.com/toby3d/telegram"
// Bot is a main object of Telegram bot
var Bot *tg.Bot
// New just create new bot by configuration credentials
func New(accessToken string) (bot *tg.Bot, err error) {
return tg.New(accessToken)
}

39
internal/commands/add.go Normal file
View File

@ -0,0 +1,39 @@
package commands
import (
log "github.com/kirillDanshin/dlog"
"gitlab.com/toby3d/mypackbot/internal/bot"
"gitlab.com/toby3d/mypackbot/internal/db"
"gitlab.com/toby3d/mypackbot/internal/errors"
"gitlab.com/toby3d/mypackbot/internal/i18n"
"gitlab.com/toby3d/mypackbot/internal/models"
"gitlab.com/toby3d/mypackbot/internal/utils"
tg "gitlab.com/toby3d/telegram"
)
// Add command prepare user for adding some stickers or sets to his pack
func Add(msg *tg.Message, pack bool) {
t, err := i18n.SwitchTo(msg.From.LanguageCode)
errors.Check(err)
_, err = bot.Bot.SendChatAction(msg.Chat.ID, tg.ActionTyping)
errors.Check(err)
reply := tg.NewMessage(msg.Chat.ID, t("reply_add_sticker"))
reply.ParseMode = tg.StyleMarkdown
reply.ReplyMarkup = utils.CancelButton(t)
err = db.DB.ChangeUserState(msg.From, models.StateAddSticker)
errors.Check(err)
if pack {
reply.Text = t("reply_add_pack")
err = db.DB.ChangeUserState(msg.From, models.StateAddPack)
errors.Check(err)
}
log.Ln("Sending add reply...")
_, err = bot.Bot.SendMessage(reply)
errors.Check(err)
}

48
internal/commands/cancel.go Executable file
View File

@ -0,0 +1,48 @@
package commands
import (
"gitlab.com/toby3d/mypackbot/internal/bot"
"gitlab.com/toby3d/mypackbot/internal/db"
"gitlab.com/toby3d/mypackbot/internal/errors"
"gitlab.com/toby3d/mypackbot/internal/i18n"
"gitlab.com/toby3d/mypackbot/internal/models"
"gitlab.com/toby3d/mypackbot/internal/utils"
tg "gitlab.com/toby3d/telegram"
)
// Cancel just cancel current user operation
func Cancel(msg *tg.Message) {
t, err := i18n.SwitchTo(msg.From.LanguageCode)
errors.Check(err)
state, err := db.DB.GetUserState(msg.From)
errors.Check(err)
_, err = bot.Bot.SendChatAction(msg.Chat.ID, tg.ActionTyping)
errors.Check(err)
var text string
switch state {
case models.StateAddSticker:
text = t("cancel_add_sticker")
case models.StateAddPack:
text = t("cancel_add_pack")
case models.StateDeleteSticker:
text = t("cancel_del_sticker")
case models.StateDeletePack:
text = t("cancel_del_pack")
case models.StateReset:
text = t("cancel_reset")
default:
text = t("cancel_error")
}
err = db.DB.ChangeUserState(msg.From, models.StateNone)
errors.Check(err)
reply := tg.NewMessage(msg.Chat.ID, text)
reply.ReplyMarkup = utils.MenuKeyboard(t)
_, err = bot.Bot.SendMessage(reply)
errors.Check(err)
}

30
internal/commands/command.go Executable file
View File

@ -0,0 +1,30 @@
package commands
import (
log "github.com/kirillDanshin/dlog"
"gitlab.com/toby3d/mypackbot/internal/models"
tg "gitlab.com/toby3d/telegram"
)
// Command check's got user command
func Command(msg *tg.Message) {
log.Ln("command:", msg.Command())
switch {
case msg.IsCommandEqual(tg.CommandStart):
Start(msg)
case msg.IsCommandEqual(tg.CommandHelp):
Help(msg)
case msg.IsCommandEqual(models.CommandAddSticker):
Add(msg, false)
case msg.IsCommandEqual(models.CommandAddPack):
Add(msg, true)
case msg.IsCommandEqual(models.CommandDeleteSticker):
Delete(msg, false)
case msg.IsCommandEqual(models.CommandDeletePack):
Delete(msg, true)
case msg.IsCommandEqual(models.CommandReset):
Reset(msg)
case msg.IsCommandEqual(models.CommandCancel):
Cancel(msg)
}
}

59
internal/commands/delete.go Executable file
View File

@ -0,0 +1,59 @@
package commands
import (
"gitlab.com/toby3d/mypackbot/internal/bot"
"gitlab.com/toby3d/mypackbot/internal/db"
"gitlab.com/toby3d/mypackbot/internal/errors"
"gitlab.com/toby3d/mypackbot/internal/i18n"
"gitlab.com/toby3d/mypackbot/internal/models"
"gitlab.com/toby3d/mypackbot/internal/utils"
tg "gitlab.com/toby3d/telegram"
)
// Delete prepare user to remove some stickers or sets from his pack
func Delete(msg *tg.Message, pack bool) {
t, err := i18n.SwitchTo(msg.From.LanguageCode)
errors.Check(err)
stickers, err := db.DB.GetUserStickers(msg.From, &tg.InlineQuery{})
errors.Check(err)
_, err = bot.Bot.SendChatAction(msg.Chat.ID, tg.ActionTyping)
errors.Check(err)
if len(stickers) <= 0 {
err = db.DB.ChangeUserState(msg.From, models.StateNone)
errors.Check(err)
reply := tg.NewMessage(msg.Chat.ID, t("error_empty_del"))
reply.ReplyMarkup = utils.MenuKeyboard(t)
_, err = bot.Bot.SendMessage(reply)
errors.Check(err)
return
}
reply := tg.NewMessage(msg.Chat.ID, t("reply_del_sticker"))
reply.ParseMode = tg.StyleMarkdown
reply.ReplyMarkup = utils.CancelButton(t)
err = db.DB.ChangeUserState(msg.From, models.StateDeleteSticker)
errors.Check(err)
if pack {
err = db.DB.ChangeUserState(msg.From, models.StateDeletePack)
errors.Check(err)
reply.Text = t("reply_del_pack")
}
_, err = bot.Bot.SendMessage(reply)
errors.Check(err)
_, err = bot.Bot.SendChatAction(msg.Chat.ID, tg.ActionTyping)
errors.Check(err)
reply = tg.NewMessage(msg.Chat.ID, t("reply_switch_button"))
reply.ReplyMarkup = utils.SwitchButton(t)
_, err = bot.Bot.SendMessage(reply)
errors.Check(err)
}

40
internal/commands/help.go Executable file
View File

@ -0,0 +1,40 @@
package commands
import (
"gitlab.com/toby3d/mypackbot/internal/bot"
"gitlab.com/toby3d/mypackbot/internal/db"
"gitlab.com/toby3d/mypackbot/internal/errors"
"gitlab.com/toby3d/mypackbot/internal/i18n"
"gitlab.com/toby3d/mypackbot/internal/models"
"gitlab.com/toby3d/mypackbot/internal/utils"
tg "gitlab.com/toby3d/telegram"
)
// Help just send instructions about bot usage
func Help(msg *tg.Message) {
t, err := i18n.SwitchTo(msg.From.LanguageCode)
errors.Check(err)
err = db.DB.ChangeUserState(msg.From, models.StateNone)
errors.Check(err)
_, err = bot.Bot.SendChatAction(msg.Chat.ID, tg.ActionTyping)
errors.Check(err)
reply := tg.NewMessage(
msg.Chat.ID, t("reply_help", map[string]interface{}{
"AddStickerCommand": models.CommandAddSticker,
"AddPackCommand": models.CommandAddPack,
"DeleteStickerCommand": models.CommandDeleteSticker,
"DeletePackCommand": models.CommandDeletePack,
"ResetCommand": models.CommandReset,
"CancelCommand": models.CommandCancel,
"Username": bot.Bot.Username,
}),
)
reply.ParseMode = tg.StyleMarkdown
reply.ReplyMarkup = utils.MenuKeyboard(t)
_, err = bot.Bot.SendMessage(reply)
errors.Check(err)
}

View File

@ -0,0 +1,47 @@
package commands
import (
"gitlab.com/toby3d/mypackbot/internal/bot"
"gitlab.com/toby3d/mypackbot/internal/db"
"gitlab.com/toby3d/mypackbot/internal/errors"
"gitlab.com/toby3d/mypackbot/internal/i18n"
"gitlab.com/toby3d/mypackbot/internal/models"
"gitlab.com/toby3d/mypackbot/internal/utils"
tg "gitlab.com/toby3d/telegram"
)
// Reset prepare user to reset his pack
func Reset(msg *tg.Message) {
t, err := i18n.SwitchTo(msg.From.LanguageCode)
errors.Check(err)
stickers, err := db.DB.GetUserStickers(msg.From, &tg.InlineQuery{})
errors.Check(err)
_, err = bot.Bot.SendChatAction(msg.Chat.ID, tg.ActionTyping)
errors.Check(err)
if len(stickers) <= 0 {
err = db.DB.ChangeUserState(msg.From, models.StateNone)
errors.Check(err)
reply := tg.NewMessage(msg.Chat.ID, t("error_already_reset"))
reply.ParseMode = tg.StyleMarkdown
reply.ReplyMarkup = utils.MenuKeyboard(t)
_, err = bot.Bot.SendMessage(reply)
errors.Check(err)
return
}
err = db.DB.ChangeUserState(msg.From, models.StateReset)
errors.Check(err)
reply := tg.NewMessage(msg.Chat.ID, t("reply_reset", map[string]interface{}{
"KeyPhrase": t("key_phrase"),
"CancelCommand": models.CommandCancel,
}))
reply.ParseMode = tg.StyleMarkdown
reply.ReplyMarkup = utils.CancelButton(t)
_, err = bot.Bot.SendMessage(reply)
errors.Check(err)
}

View File

@ -0,0 +1,47 @@
package commands
import (
"strings"
log "github.com/kirillDanshin/dlog"
"gitlab.com/toby3d/mypackbot/internal/bot"
"gitlab.com/toby3d/mypackbot/internal/db"
"gitlab.com/toby3d/mypackbot/internal/errors"
"gitlab.com/toby3d/mypackbot/internal/i18n"
"gitlab.com/toby3d/mypackbot/internal/models"
"gitlab.com/toby3d/mypackbot/internal/utils"
tg "gitlab.com/toby3d/telegram"
)
// Start just send introduction about bot to user
func Start(msg *tg.Message) {
err := db.DB.ChangeUserState(msg.From, models.StateNone)
errors.Check(err)
_, err = bot.Bot.SendChatAction(msg.Chat.ID, tg.ActionTyping)
errors.Check(err)
if msg.HasCommandArgument() {
log.Ln("Received a", msg.Command(), "command with", msg.CommandArgument(), "argument")
if strings.EqualFold(msg.CommandArgument(), tg.CommandHelp) {
Help(msg)
return
}
}
t, err := i18n.SwitchTo(msg.From.LanguageCode)
errors.Check(err)
reply := tg.NewMessage(
msg.Chat.ID,
t("reply_start", map[string]interface{}{
"Username": bot.Bot.Username,
"ID": bot.Bot.ID,
}),
)
reply.ParseMode = tg.StyleMarkdown
reply.ReplyMarkup = utils.MenuKeyboard(t)
_, err = bot.Bot.SendMessage(reply)
errors.Check(err)
}

17
internal/config/open.go Normal file
View File

@ -0,0 +1,17 @@
package config
import "github.com/spf13/viper"
var Config *viper.Viper
// Open just open configuration file for parsing some data in other functions
func Open(path string) (*viper.Viper, error) {
cfg := viper.New()
cfg.AddConfigPath(path)
cfg.SetConfigName("config")
cfg.SetConfigType("yaml")
err := cfg.ReadInConfig()
return cfg, err
}

View File

@ -0,0 +1,36 @@
package db
import (
"fmt"
log "github.com/kirillDanshin/dlog"
"github.com/tidwall/buntdb"
"gitlab.com/toby3d/mypackbot/internal/models"
tg "gitlab.com/toby3d/telegram"
)
// AddSticker add sticker FileID, Emoji and SetName meta for UserID
func (db *DataBase) AddSticker(user *tg.User, sticker *tg.Sticker) (bool, error) {
log.Ln("Trying to add", sticker.FileID, "sticker from", user.ID, "user")
if sticker.SetName == "" {
sticker.SetName = models.SetUploaded
}
var exists bool
err := db.Update(func(tx *buntdb.Tx) error {
var err error
_, exists, err = tx.Set(
fmt.Sprint("user:", user.ID, ":set:", sticker.SetName, ":sticker:", sticker.FileID), // key
sticker.Emoji, // value
nil, // options
)
if err == buntdb.ErrIndexExists {
exists = true
return nil
}
return err
})
return exists, err
}

View File

@ -0,0 +1,18 @@
package db
import (
"fmt"
log "github.com/kirillDanshin/dlog"
"github.com/tidwall/buntdb"
tg "gitlab.com/toby3d/telegram"
)
// ChangeUserState change current user state on input state.
func (db *DataBase) ChangeUserState(user *tg.User, state string) error {
log.Ln("Trying to change", user.ID, "state to", state)
return db.Update(func(tx *buntdb.Tx) error {
_, _, err := tx.Set(fmt.Sprint("user:", user.ID, ":state"), state, nil)
return err
})
}

View File

@ -0,0 +1,50 @@
package db
import (
"fmt"
"strings"
log "github.com/kirillDanshin/dlog"
"github.com/tidwall/buntdb"
"gitlab.com/toby3d/mypackbot/internal/models"
tg "gitlab.com/toby3d/telegram"
)
// DeletePack remove all keys for UserID which contains input SetName
func (db *DataBase) DeletePack(user *tg.User, sticker *tg.Sticker) (bool, error) {
log.Ln("Trying to remove all", sticker.SetName, "sticker from", user.ID, "user")
if sticker.SetName == "" {
sticker.SetName = models.SetUploaded
}
var ids []string
err := db.View(func(tx *buntdb.Tx) error {
return tx.AscendKeys(
fmt.Sprint("user:", user.ID, ":set:", sticker.SetName, ":*"),
func(key, val string) bool {
keys := strings.Split(key, ":")
ids = append(ids, keys[5])
return true
},
)
})
if len(ids) == 0 {
return true, nil
}
for _, id := range ids {
var notExist bool
notExist, err = db.DeleteSticker(user, &tg.Sticker{FileID: id})
if err != nil {
return notExist, err
}
}
if err == buntdb.ErrNotFound {
log.Ln(user.ID, "not found")
return true, nil
}
return false, err
}

View File

@ -0,0 +1,31 @@
package db
import (
"fmt"
log "github.com/kirillDanshin/dlog"
"github.com/tidwall/buntdb"
"gitlab.com/toby3d/mypackbot/internal/models"
tg "gitlab.com/toby3d/telegram"
)
// DeleteSticker just remove specified sticker key from database.
func (db *DataBase) DeleteSticker(user *tg.User, sticker *tg.Sticker) (bool, error) {
log.Ln("Trying to remove", sticker.FileID, "sticker from", user.ID, "user")
if sticker.SetName == "" {
sticker.SetName = models.SetUploaded
}
err := db.Update(func(tx *buntdb.Tx) error {
_, err := tx.Delete(
fmt.Sprint("user:", user.ID, ":set:", sticker.SetName, ":sticker:", sticker.FileID),
)
return err
})
if err == buntdb.ErrNotFound {
log.Ln(user.ID, "not found, create new one")
return true, nil
}
return false, err
}

View File

@ -0,0 +1,29 @@
package db
import (
"fmt"
log "github.com/kirillDanshin/dlog"
"github.com/tidwall/buntdb"
"gitlab.com/toby3d/mypackbot/internal/models"
tg "gitlab.com/toby3d/telegram"
)
// UserState return current state for UserID
func (db *DataBase) GetUserState(user *tg.User) (string, error) {
log.Ln("Trying to get", user.ID, "state")
var state string
err := db.View(func(tx *buntdb.Tx) error {
var err error
state, err = tx.Get(fmt.Sprint("user:", user.ID, ":state"))
return err
})
if err == buntdb.ErrNotFound {
log.Ln(user.ID, "not found, create new one")
if err = db.ChangeUserState(user, models.StateNone); err != nil {
return state, err
}
}
return state, err
}

View File

@ -0,0 +1,55 @@
package db
import (
"fmt"
"strconv"
"strings"
log "github.com/kirillDanshin/dlog"
"github.com/tidwall/buntdb"
tg "gitlab.com/toby3d/telegram"
)
// GetUserStickers return array of saved stickers for input UserID and his total count
func (db *DataBase) GetUserStickers(user *tg.User, query *tg.InlineQuery) ([]string, error) {
log.Ln("Trying to get", user.ID, "stickers")
var i int
var stickers []string
offset, _ := strconv.Atoi(query.Offset)
offset *= 50
err := db.View(func(tx *buntdb.Tx) error {
return tx.AscendKeys(
fmt.Sprint("user:", user.ID, ":set:*"), // index
func(key, val string) bool { // iterator
subKeys := strings.Split(key, ":")
if subKeys[1] != strconv.Itoa(user.ID) {
return true
}
if len(stickers) == 50 {
return false
}
i++
if i < offset {
return true
}
if query.Query != "" && !strings.Contains(query.Query, val) {
return true
}
stickers = append(stickers, subKeys[5])
return true
},
)
})
if err == buntdb.ErrNotFound {
log.Ln("Not found stickers")
return nil, nil
}
return stickers, err
}

33
internal/db/get_users.go Normal file
View File

@ -0,0 +1,33 @@
package db
import (
"strconv"
"strings"
// log "github.com/kirillDanshin/dlog"
"github.com/tidwall/buntdb"
)
// GetUsers return array of all available UserID in database
func (db *DataBase) GetUsers() ([]int, error) {
var users []int
err := db.View(func(tx *buntdb.Tx) error {
return tx.AscendKeys(
"user:*:state",
func(key, val string) bool {
subKeys := strings.Split(key, ":")
id, err := strconv.Atoi(subKeys[1])
if err == nil {
users = append(users, id)
}
return true
},
)
})
if err == buntdb.ErrNotFound {
return nil, nil
}
return users, err
}

18
internal/db/open.go Normal file
View File

@ -0,0 +1,18 @@
package db
import (
log "github.com/kirillDanshin/dlog"
"github.com/tidwall/buntdb"
)
type DataBase struct{ *buntdb.DB }
// DB is a main object of current database connection
var DB *DataBase
// Open just open connection to database for work
func Open(path string) (*DataBase, error) {
log.Ln("Open database file...")
db, err := buntdb.Open(path)
return &DataBase{db}, err
}

40
internal/db/reset.go Normal file
View File

@ -0,0 +1,40 @@
package db
import (
"fmt"
"strconv"
"strings"
log "github.com/kirillDanshin/dlog"
"github.com/tidwall/buntdb"
tg "gitlab.com/toby3d/telegram"
)
// ResetUser just drop out all stickers keys for input UserID
func (db *DataBase) ResetUser(user *tg.User) error {
log.Ln("Trying reset all stickers of", user.ID, "user")
return db.Update(func(tx *buntdb.Tx) error {
var keys []string
if err := tx.AscendKeys(
fmt.Sprint("user:", user.ID, ":set:*"), // index
func(key, val string) bool { // iterator
subKeys := strings.Split(key, ":")
if subKeys[1] == strconv.Itoa(user.ID) {
keys = append(keys, key)
}
return true
},
); err != nil {
return err
}
for i := range keys {
_, err := tx.Delete(keys[i])
if err != nil {
break
}
}
return nil
})
}

30
internal/db/user_state.go Normal file
View File

@ -0,0 +1,30 @@
package db
import (
"fmt"
log "github.com/kirillDanshin/dlog"
"github.com/tidwall/buntdb"
"gitlab.com/toby3d/mypackbot/internal/models"
tg "gitlab.com/toby3d/telegram"
)
// UserState return current state for UserID
func (db *DataBase) UserState(usr *tg.User) (string, error) {
log.Ln("Trying to get", usr.ID, "state")
var state string
err := DB.View(func(tx *buntdb.Tx) error {
var err error
state, err = tx.Get(fmt.Sprint("user:", usr.ID, ":state"))
return err
})
if err == buntdb.ErrNotFound {
log.Ln(usr.ID, "not found, create new one")
if err = db.ChangeUserState(usr, models.StateNone); err != nil {
return state, err
}
}
return state, err
}

32
internal/errors/check.go Normal file
View File

@ -0,0 +1,32 @@
package errors
import (
"log"
"log/syslog"
"os"
"sync"
)
var (
// WaitForwards is a wait group which wait send all announcements before panic
WaitForwards = new(sync.WaitGroup)
sysLogger *syslog.Writer
)
// Check helps debug critical errors without warnings from 'gocyclo' linter
func Check(err error) {
if err != nil {
log.Println(err.Error())
// Wait what all users get announcement message first
WaitForwards.Wait()
err = sysLogger.Crit(err.Error())
if err != nil {
log.Panicln(err.Error())
}
os.Exit(1)
}
}

20
internal/i18n/open.go Normal file
View File

@ -0,0 +1,20 @@
package i18n
import (
"os"
"path/filepath"
"strings"
"github.com/nicksnyder/go-i18n/i18n"
)
// Open just walk in input path for preloading localization files
func Open(path string) error {
return filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if !strings.HasSuffix(path, ".all.yaml") {
return err
}
return i18n.LoadTranslationFile(path)
})
}

View File

@ -0,0 +1,13 @@
package i18n
import (
"github.com/nicksnyder/go-i18n/i18n"
"gitlab.com/toby3d/mypackbot/internal/models"
)
// SwitchTo try load locales by input language codes and return TranslateFunc
func SwitchTo(codes ...string) (t i18n.TranslateFunc, err error) {
codes = append(codes, models.LanguageFallback)
t, err = i18n.Tfunc(codes[0], codes[1:]...)
return
}

View File

@ -0,0 +1,35 @@
package messages
import (
"strings"
"gitlab.com/toby3d/mypackbot/internal/actions"
"gitlab.com/toby3d/mypackbot/internal/commands"
"gitlab.com/toby3d/mypackbot/internal/errors"
"gitlab.com/toby3d/mypackbot/internal/i18n"
tg "gitlab.com/toby3d/telegram"
)
// Message checks user message on response, stickers, reset key phrase, else do
// Actions
func Message(msg *tg.Message) {
t, err := i18n.SwitchTo(msg.From.LanguageCode)
errors.Check(err)
switch {
case strings.EqualFold(msg.Text, t("button_add_sticker")):
commands.Add(msg, false)
case strings.EqualFold(msg.Text, t("button_add_pack")):
commands.Add(msg, true)
case strings.EqualFold(msg.Text, t("button_del_sticker")):
commands.Delete(msg, false)
case strings.EqualFold(msg.Text, t("button_del_pack")):
commands.Delete(msg, true)
case strings.EqualFold(msg.Text, t("button_reset")):
commands.Reset(msg)
case strings.EqualFold(msg.Text, t("button_cancel")):
commands.Cancel(msg)
default:
actions.Action(msg)
}
}

37
internal/models/models.go Normal file
View File

@ -0,0 +1,37 @@
package models
import tg "gitlab.com/toby3d/telegram"
// Commands... represents available and supported bot commands
const (
CommandAddPack = "addPack"
CommandAddSticker = "addSticker"
CommandCancel = "cancel"
CommandDeleteSticker = "delSticker"
CommandDeletePack = "delPack"
CommandReset = "reset"
)
// State... represents current state of user action
const (
StateNone = "none"
StateAddSticker = CommandAddSticker
StateAddPack = CommandAddPack
StateDeleteSticker = CommandDeleteSticker
StateDeletePack = CommandDeletePack
StateReset = CommandReset
)
// SetUploaded is a mimic set name of uploaded stickers without any parent set
const SetUploaded = "?"
// LanguageFallback is a default language code in case what current user language
// is not support yet
const LanguageFallback = "en"
// AllowedUpdates is
var AllowedUpdates = []string{
tg.UpdateInlineQuery, // For searching and sending stickers
tg.UpdateMessage, // For get commands and messages
tg.UpdateChannelPost, // For forwarding announcements
}

View File

@ -0,0 +1,68 @@
package updates
import (
"fmt"
"net/url"
log "github.com/kirillDanshin/dlog"
"gitlab.com/toby3d/mypackbot/internal/bot"
"gitlab.com/toby3d/mypackbot/internal/config"
"gitlab.com/toby3d/mypackbot/internal/models"
tg "gitlab.com/toby3d/telegram"
)
// Channel return webhook or long polling channel with bot updates
func Channel(webhookMode bool) (updates tg.UpdatesChannel, err error) {
log.Ln("Preparing channel for updates...")
if !webhookMode {
log.Ln("Use LongPolling updates")
var info *tg.WebhookInfo
info, err = bot.Bot.GetWebhookInfo()
if err != nil {
return
}
if info.URL != "" {
log.Ln("Deleting webhook...")
_, err = bot.Bot.DeleteWebhook()
return
}
updates = bot.Bot.NewLongPollingChannel(&tg.GetUpdatesParameters{
Offset: 0,
Limit: 100,
Timeout: 60,
AllowedUpdates: models.AllowedUpdates,
})
return
}
set, err := url.Parse(config.Config.GetString("telegram.webhook.set"))
if err != nil {
return nil, err
}
listen := config.Config.GetString("telegram.webhook.listen")
serve := config.Config.GetString("telegram.webhook.serve")
log.Ln(
"Trying set webhook on address:",
fmt.Sprint(set.String(), bot.Bot.AccessToken),
)
log.Ln("Creating new webhook...")
params := tg.NewWebhook(fmt.Sprint(set, listen, bot.Bot.AccessToken), nil)
params.MaxConnections = 40
params.AllowedUpdates = models.AllowedUpdates
updates = bot.Bot.NewWebhookChannel(
set,
params, // params
"", // certFile
"", // keyFile
serve, // serve
)
return
}

View File

@ -0,0 +1,36 @@
package updates
import (
"time"
log "github.com/kirillDanshin/dlog"
"gitlab.com/toby3d/mypackbot/internal/bot"
"gitlab.com/toby3d/mypackbot/internal/config"
"gitlab.com/toby3d/mypackbot/internal/db"
"gitlab.com/toby3d/mypackbot/internal/errors"
tg "gitlab.com/toby3d/telegram"
)
// ChannelPost checks ChannelPost update for forwarding content to bot users
func ChannelPost(post *tg.Message) {
channelID := config.Config.GetInt64("telegram.channel")
if post.Chat.ID != channelID {
log.Ln(post.Chat.ID, "!=", channelID)
return
}
users, err := db.DB.GetUsers()
errors.Check(err)
for i := range users {
errors.WaitForwards.Add(1)
if _, err = bot.Bot.ForwardMessage(
tg.NewForwardMessage(post.Chat.ID, int64(users[i]), post.ID),
); err != nil {
log.Ln(err.Error())
}
errors.WaitForwards.Done()
time.Sleep(time.Second / 10) // For avoid spamming
}
}

View File

@ -0,0 +1,86 @@
package updates
import (
"strconv"
log "github.com/kirillDanshin/dlog"
"gitlab.com/toby3d/mypackbot/internal/bot"
"gitlab.com/toby3d/mypackbot/internal/db"
"gitlab.com/toby3d/mypackbot/internal/errors"
"gitlab.com/toby3d/mypackbot/internal/i18n"
"gitlab.com/toby3d/mypackbot/internal/utils"
tg "gitlab.com/toby3d/telegram"
)
// InlineQuery checks InlineQuery updates for answer with personal results
func InlineQuery(inlineQuery *tg.InlineQuery) {
fixedQuery, err := utils.FixEmoji(inlineQuery.Query)
if err == nil {
inlineQuery.Query = fixedQuery
}
answer := new(tg.AnswerInlineQueryParameters)
answer.InlineQueryID = inlineQuery.ID
answer.CacheTime = 1
answer.IsPersonal = true
if len([]rune(inlineQuery.Query)) >= 256 {
_, err = bot.Bot.AnswerInlineQuery(answer)
errors.Check(err)
return
}
log.Ln("Let's preparing answer...")
t, err := i18n.SwitchTo(inlineQuery.From.LanguageCode)
errors.Check(err)
log.Ln("INLINE OFFSET:", inlineQuery.Offset)
if inlineQuery.Offset == "" {
inlineQuery.Offset = "-1"
}
offset, err := strconv.Atoi(inlineQuery.Offset)
errors.Check(err)
offset++
stickers, err := db.DB.GetUserStickers(
inlineQuery.From,
&tg.InlineQuery{
Offset: strconv.Itoa(offset),
Query: inlineQuery.Query,
},
)
errors.Check(err)
if len(stickers) == 0 {
if offset == 0 && inlineQuery.Query != "" {
// If search stickers by emoji return 0 results
answer.SwitchPrivateMessageText = t(
"button_inline_nothing", map[string]interface{}{
"Query": inlineQuery.Query,
},
)
answer.SwitchPrivateMessageParameter = tg.CommandHelp
}
answer.Results = nil
} else {
log.Ln("STICKERS FROM REQUEST:", len(stickers))
if len(stickers) == 50 {
answer.NextOffset = strconv.Itoa(offset)
log.Ln("NEXT OFFSET:", answer.NextOffset)
}
var results = make([]interface{}, len(stickers))
for i, sticker := range stickers {
results[i] = tg.NewInlineQueryResultCachedSticker(sticker, sticker)
}
answer.Results = results
}
log.Ln("CacheTime:", answer.CacheTime)
_, err = bot.Bot.AnswerInlineQuery(answer)
errors.Check(err)
}

View File

@ -0,0 +1,29 @@
package updates
import (
log "github.com/kirillDanshin/dlog"
"gitlab.com/toby3d/mypackbot/internal/actions"
"gitlab.com/toby3d/mypackbot/internal/bot"
"gitlab.com/toby3d/mypackbot/internal/commands"
"gitlab.com/toby3d/mypackbot/internal/messages"
tg "gitlab.com/toby3d/telegram"
)
// Message checks Message updates for answer to user commands, replies or sended
// stickers
func Message(msg *tg.Message) {
if bot.Bot.IsMessageFromMe(msg) ||
bot.Bot.IsForwardFromMe(msg) {
log.Ln("Ignore message update")
return
}
switch {
case bot.Bot.IsCommandToMe(msg):
commands.Command(msg)
case msg.IsText():
messages.Message(msg)
default:
actions.Action(msg)
}
}

View File

@ -0,0 +1,15 @@
package utils
import (
"github.com/nicksnyder/go-i18n/i18n"
tg "gitlab.com/toby3d/telegram"
)
// CancelButton helper just generate ReplyMarkup with cancel button
func CancelButton(t i18n.TranslateFunc) *tg.ReplyKeyboardMarkup {
return tg.NewReplyKeyboardMarkup(
tg.NewReplyKeyboardRow(
tg.NewReplyKeyboardButton(t("button_cancel")),
),
)
}

View File

@ -0,0 +1,27 @@
package utils
import (
"golang.org/x/text/runes"
"golang.org/x/text/transform"
)
// Skin colors for remove
var bannedSkins = []rune{127995, 127996, 127997, 127998, 127999}
// Transformer for remove skin colors
var skinRemover = runes.Remove(runes.Predicate(
func(r rune) bool {
for _, skin := range bannedSkins {
if r == skin {
return true
}
}
return false
},
))
// FixEmoji fixes user input by remove all potential skin colors
func FixEmoji(raw string) (string, error) {
result, _, err := transform.String(skinRemover, raw)
return result, err
}

View File

@ -0,0 +1,23 @@
package utils
import (
"github.com/nicksnyder/go-i18n/i18n"
tg "gitlab.com/toby3d/telegram"
)
// MenuKeyboard helper just generate ReplyMarkup with menu buttons
func MenuKeyboard(t i18n.TranslateFunc) *tg.ReplyKeyboardMarkup {
return tg.NewReplyKeyboardMarkup(
tg.NewReplyKeyboardRow(
tg.NewReplyKeyboardButton(t("button_add_sticker")),
tg.NewReplyKeyboardButton(t("button_add_pack")),
),
tg.NewReplyKeyboardRow(
tg.NewReplyKeyboardButton(t("button_del_sticker")),
tg.NewReplyKeyboardButton(t("button_del_pack")),
),
tg.NewReplyKeyboardRow(
tg.NewReplyKeyboardButton(t("button_reset")),
),
)
}

View File

@ -0,0 +1,15 @@
package utils
import (
"github.com/nicksnyder/go-i18n/i18n"
tg "gitlab.com/toby3d/telegram"
)
// SwitchButton helper just generate ReplyMarkup with SelfSwitch button
func SwitchButton(t i18n.TranslateFunc) *tg.InlineKeyboardMarkup {
return tg.NewInlineKeyboardMarkup(
tg.NewInlineKeyboardRow(
tg.NewInlineKeyboardButtonSwitchSelf(t("button_inline_select"), " "),
),
)
}

102
main.go
View File

@ -1,73 +1,57 @@
package main
import (
log "github.com/kirillDanshin/dlog" // Insert logs only in debug builds
"github.com/toby3d/botan"
"github.com/toby3d/go-telegram" // My Telegram bindings
"flag"
log "github.com/kirillDanshin/dlog"
"gitlab.com/toby3d/mypackbot/internal/bot"
"gitlab.com/toby3d/mypackbot/internal/config"
"gitlab.com/toby3d/mypackbot/internal/db"
"gitlab.com/toby3d/mypackbot/internal/errors"
"gitlab.com/toby3d/mypackbot/internal/i18n"
"gitlab.com/toby3d/mypackbot/internal/updates"
)
// bot is general structure of the bot
var bot *telegram.Bot
var flagWebhook = flag.Bool(
"webhook", false,
"enable work via webhooks (required valid certificates)",
)
// init prepare configuration and other things for successful start
func init() {
log.Ln("Initializing...")
// Preload localization strings
err := i18n.Open("i18n/")
errors.Check(err)
// Preload configuration file
config.Open("configs/config.yaml")
// Open database or create new one
db.Open("stickers.db")
// Create bot with credentials from config
bot.Bot, err = bot.New(config.Config.GetString("telegram.token"))
errors.Check(err)
}
// main function is a general function for work of this bot
func main() {
log.Ln("Let'g Get It Started...")
var err error
flag.Parse() // Parse flagWebhook
go dbInit()
channel, err := updates.Channel(*flagWebhook)
errors.Check(err)
log.Ln("Initializing new bot via checking access_token...")
bot, err = telegram.NewBot(cfg.UString("telegram.token"))
errCheck(err)
log.Ln("Let's check updates channel!")
for update := range getUpdatesChannel() {
for update := range channel {
log.D(update)
switch {
case update.ChosenInlineResult != nil:
log.Ln("Get ChosenInlineResult update")
appMetrika.Track(
"Chosen inline result",
update.ChosenInlineResult.From.ID,
*update.ChosenInlineResult,
)
case update.InlineQuery != nil:
// Just don't check same updates
log.D(update.InlineQuery.Query)
if len(update.InlineQuery.Query) > 25 {
continue
}
appMetrika.TrackAsync(
"Inline query", update.InlineQuery.From.ID, *update.InlineQuery,
func(answer *botan.Answer, err error) {
log.Ln("Asynchonous:", answer.Status)
metrika <- true
},
)
inlineQuery(update.InlineQuery)
<-metrika
case update.Message != nil:
if update.Message.From.ID == bot.Self.ID {
log.Ln("Received a message from myself, ignore this update")
return
}
if update.Message.ForwardFrom != nil {
if update.Message.ForwardFrom.ID == bot.Self.ID {
log.Ln("Received a forward from myself, ignore this update")
return
}
}
messages(update.Message)
default:
log.Ln("Get unsupported update")
case update.IsInlineQuery():
updates.InlineQuery(update.InlineQuery)
case update.IsMessage():
updates.Message(update.Message)
case update.IsChannelPost():
updates.ChannelPost(update.ChannelPost)
}
continue
}
err = db.Close()
errCheck(err)
}

View File

@ -1,136 +0,0 @@
package main
import (
log "github.com/kirillDanshin/dlog" // Insert logs only in debug builds
"github.com/toby3d/botan"
"github.com/toby3d/go-telegram" // My Telegram bindings
)
// message function check Message update on commands, sended stickers or other
// user stuff
func messages(msg *telegram.Message) {
if msg.IsCommand() {
commands(msg)
return
}
state, err := dbGetUserState(msg.From.ID)
errCheck(err)
switch state {
case stateNone:
appMetrika.TrackAsync(
"Nothing", msg.From.ID, *msg,
func(answer *botan.Answer, err error) {
log.Ln("Asynchonous:", answer.Status)
metrika <- true
},
)
bot.SendChatAction(msg.Chat.ID, telegram.ActionTyping)
T, err := switchLocale(msg.From.LanguageCode)
errCheck(err)
reply := telegram.NewMessage(
msg.Chat.ID,
T("error_unknown", map[string]interface{}{
"AddStickerCommand": cmdAddSticker,
"AddPackCommand": cmdAddPack,
"DeleteCommand": cmdDelete,
}))
reply.ParseMode = telegram.ModeMarkdown
_, err = bot.SendMessage(reply)
errCheck(err)
<-metrika
case stateAddSticker:
if msg.Sticker == nil {
appMetrika.Track("Message", msg.From.ID, *msg)
return
}
log.D(msg.Sticker)
log.D(msg.Sticker.Emoji)
appMetrika.TrackAsync(
"Add single sticker", msg.From.ID, *msg,
func(answer *botan.Answer, err error) {
log.Ln("Asynchonous:", answer.Status)
metrika <- true
},
)
actionAdd(msg, false)
<-metrika
case stateAddPack:
if msg.Sticker == nil {
appMetrika.Track("Message", msg.From.ID, *msg)
return
}
appMetrika.TrackAsync(
"Add pack", msg.From.ID, *msg,
func(answer *botan.Answer, err error) {
log.Ln("Asynchonous:", answer.Status)
metrika <- true
},
)
log.D(msg.Sticker)
log.D(msg.Sticker.Emoji)
actionAdd(msg, true)
<-metrika
case stateDelete:
if msg.Sticker == nil {
appMetrika.Track("Message", msg.From.ID, *msg)
return
}
appMetrika.TrackAsync(
"Delete sticker", msg.From.ID, *msg,
func(answer *botan.Answer, err error) {
log.Ln("Asynchonous:", answer.Status)
metrika <- true
},
)
log.D(msg.Sticker)
log.D(msg.Sticker.Emoji)
actionDelete(msg)
<-metrika
case stateReset:
appMetrika.TrackAsync(
"Reset pack", msg.From.ID, *msg,
func(answer *botan.Answer, err error) {
log.Ln("Asynchonous:", answer.Status)
metrika <- true
},
)
actionReset(msg)
<-metrika
default:
appMetrika.TrackAsync(
"Message", msg.From.ID, *msg,
func(answer *botan.Answer, err error) {
log.Ln("Asynchonous:", answer.Status)
metrika <- true
},
)
err = dbChangeUserState(msg.From.ID, stateNone)
errCheck(err)
messages(msg)
<-metrika
}
}

View File

@ -1,88 +0,0 @@
package main
import (
"fmt"
"time"
"github.com/toby3d/go-telegram" // My Telegram bindings
)
const keyPhrase = "Yes, I am totally sure."
func commandReset(msg *telegram.Message) {
bot.SendChatAction(msg.Chat.ID, telegram.ActionTyping)
T, err := switchLocale(msg.From.LanguageCode)
errCheck(err)
stickers, _, err := dbGetUserStickers(msg.From.ID)
errCheck(err)
if len(stickers) <= 0 {
err = dbChangeUserState(msg.From.ID, stateNone)
errCheck(err)
reply := telegram.NewMessage(msg.Chat.ID, T("error_already_reset"))
reply.ParseMode = telegram.ModeMarkdown
_, err = bot.SendMessage(reply)
errCheck(err)
return
}
err = dbChangeUserState(msg.From.ID, stateReset)
errCheck(err)
reply := telegram.NewMessage(
msg.Chat.ID,
T("reply_reset", map[string]interface{}{
"KeyPhrase": keyPhrase,
"CancelCommand": cmdCancel,
}))
reply.ParseMode = telegram.ModeMarkdown
_, err = bot.SendMessage(reply)
errCheck(err)
}
func actionReset(msg *telegram.Message) {
bot.SendChatAction(msg.Chat.ID, telegram.ActionTyping)
err := dbChangeUserState(msg.From.ID, stateNone)
errCheck(err)
T, err := switchLocale(msg.From.LanguageCode)
errCheck(err)
if msg.Text != keyPhrase {
reply := telegram.NewMessage(msg.Chat.ID, T("error_reset_phrase"))
reply.ParseMode = telegram.ModeMarkdown
_, err = bot.SendMessage(reply)
errCheck(err)
return
}
err = dbResetUserStickers(msg.From.ID)
errCheck(err)
reply := telegram.NewMessage(msg.Chat.ID, T("success_reset"))
reply.ParseMode = telegram.ModeMarkdown
_, err = bot.SendMessage(reply)
errCheck(err)
for i := 1; i <= 3; i++ {
bot.SendChatAction(msg.Chat.ID, telegram.ActionTyping)
text := T(fmt.Sprint("meta_reset_", i))
time.Sleep(time.Minute * time.Duration(len(text)) / 1000)
reply = telegram.NewMessage(msg.Chat.ID, text)
reply.ParseMode = telegram.ModeMarkdown
_, err = bot.SendMessage(reply)
errCheck(err)
}
}

View File

@ -1,37 +0,0 @@
package main
import (
"strings"
log "github.com/kirillDanshin/dlog" // Insert logs only in debug builds
"github.com/toby3d/go-telegram" // My Telegram bindings
)
func commandStart(msg *telegram.Message) {
bot.SendChatAction(msg.Chat.ID, telegram.ActionTyping)
err := dbChangeUserState(msg.From.ID, stateNone)
errCheck(err)
if msg.HasArgument() {
log.Ln("Received a", msg.Command(), "command with", msg.CommandArgument(), "argument")
if strings.ToLower(msg.CommandArgument()) == strings.ToLower(cmdAddSticker) {
commandAdd(msg, false)
return
}
}
T, err := switchLocale(msg.From.LanguageCode)
errCheck(err)
reply := telegram.NewMessage(
msg.Chat.ID, T("reply_start", map[string]interface{}{
"Username": bot.Self.Username,
"ID": bot.Self.ID,
}),
)
reply.ParseMode = telegram.ModeMarkdown
_, err = bot.SendMessage(reply)
errCheck(err)
}

View File

@ -1,18 +0,0 @@
package main
import (
log "github.com/kirillDanshin/dlog" // Insert logs only in debug builds
"github.com/nicksnyder/go-i18n/i18n" // Internationalization and localization
)
const langDefault = "en-us"
func switchLocale(langCode string) (T i18n.TranslateFunc, err error) {
log.Ln("Check", langCode, "localization")
T, err = i18n.Tfunc(langCode)
if err != nil {
log.Ln("Unsupported language, change to 'en-us' by default")
T, err = i18n.Tfunc(langDefault)
}
return
}