diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9d82e02 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +.idea +.mvn +Dockerfile \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..9806346 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +BOT_USER_NAME=GroupControlSkillboxBot +BOT_TOKEN=5566628073:AAHyZTYOh62Fb7_zVGNmHMqzwJUQtBvGWuc \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..bf82ff0 Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..70e554c --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# https://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. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..039a84f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM openjdk:17-alpine +COPY target/tg-bot-users-0.0.1-SNAPSHOT.jar tg-bot-users.jar +COPY application-bot.yml . +CMD java -jar tg-bot-users.jar \ No newline at end of file diff --git a/README.md b/README.md index 1164b0e..7684da0 100644 --- a/README.md +++ b/README.md @@ -1 +1,138 @@ # tg-bot-users 🤖 + +--- +## Общие сведения +Приложение является телеграм ботом + + +Основные функции: + * Бот отслеживает вступление в группу новых пользователей. + * В случае юбилейного вступления пользователя, присылает уведомление в группу модераторов для принятия решения о поздравлении. + * При нажатии кнопки "Поздравить" приходит автоматическое поздравление вступившего пользователя. + * При нажатии кнопки "Отклонить" поздравления не происходит. + + +Дополнительные функции: + * Помимо самого юбилейного вступления, отслеживается несколько последующих - для ситуаций, если юбилейным оказался бот или модератор. Уведомление присылается на каждое подобное вступление, если решение о поздравлении ещё не было принято. + * Модератор может запросить у бота список всех юбилейных вступлений. Если в юбилейном списке есть ожидающие поздравления, то модератор может принять решение, пользуясь полученным списком. При этом раннее отклоненные решения могут быть изменены. + + +Менять настройки групп могут только администраторы. + +--- +## Запуск +Проект настроен так, чтобы администратору приходилось выполнять минимум действий, поэтому сконфигурированное приложение запускается лишь парой команд. Нет необходимости настраивать базу данных, так как она запускается в docker-контейнере рядом с контейнером приложения. И так для запуска приложения вам потребуется выполнить следующие действия: + +Все команды выполняются в терминале в корне проекта! +* Сделать клонирование репозитория командой `git clone {адрес репозитория}` +* Настроить соответствующие файлы конфигураций: + * .env - установить следующие значения: + * `BOT_USER_NAME` - Имя бота + * `BOT_TOKEN` - Токен бота (необходимо создать нового бота по [инструкции](https://core.telegram.org/bots#3-how-do-i-create-a-bot)) + * `DATASOURCE_DB` - Название базы данных (опционально) + * `DATASOURCE_URL` - url базы данных со слэшом в конце "/" (опционально) + * `DATASOURCE_USER` - Имя пользователя базы данных (опционально) + * `DATASOURCE_PASS` - Пароль пользователя базы данных (опционально) + * application-bot.yml - установить значения для начальной настройки администраторов, чатов пользователей и модераторов и т.д. +* С помощью Maven собрать проект командой `mvn clean package` +* Установить и запустить Docker, затем выполнить команду `docker-compose up` или `docker-compose up -d` (для запуска в фоновом режиме) + +--- +## Команды бота + +### Команды администраторов +_Доступны администраторам в приватном чате_ + +| Команда | Описание | +|------------------------------------|---------------------------------------------------------| +| /help | вывод списка доступных команд | +| /currentSettings | вывод текущих настроек чатов | +| /addModerChat {id} | добавление чата модераторов | +| /addUserChat {id} | добавление чата пользователей | +| /deleteModerChat {id} | удаление чата модераторов | +| /deleteUserChat {id} | удаление чата пользователей | +| /bindUserChatToModer {id} {id} | привязка чата пользователей к чату модераторов | +| /unbindUserChatFromModer {id} {id} | удаление привязки чата пользователей к чату модераторов | + + +### Команды модераторов +_Доступны в чатах модераторов_ + +| Команда | Описание | +|---------------------|---------------------------------------------------------------------------------| +| /luckyList | вывод списка юбилейных вступлений во всех привязанных чатах пользователей | +| /luckyList {id} | вывод списка юбилейных вступлений в конкретном чате | +| /chooseLucky | вывод списка **ожидающих поздравления** во всех привязанных чатах пользователей | +| /chooseLucky {id} | вывод списка **ожидающих поздравления** в конкретном чате | + + +--- +## Web API +На всякий случай для бота сделан небольшой API.
По умолчанию используется порт 8080. + +| Endpoint | Описание | +|------------------|-------------------------------------------------------------------------------------------------| +| /api/start | Ручной старт бота | +| /api/stop | Ручная остановка бота | +| /api/status | Текущий статус бота | +| /api/sendMessage | Отправка сообщения от бота
`chatId` ID чата, куда отправить
`message` текст сообщения | + + +--- +## Настройки приложения + +Конфигурационные файлы приложения разделены на две части: +### Настройки для администраторов _(application.yml)_: + * Spring + * Database _(environment vars)_ + * Bot token _(environment vars)_ + + +### Настройки функций бота _(application-bot.yml)_: +Настройки групп в приоритете берутся из базы данных. Из файла конфигурации эти настройки подтягиваются только в случае отсутствия таковых в БД, либо если включен флаг перезаписи настроек. + + * Настройки для чатов `chats-settings`: + * `administrators` - список ID администраторов + * `anniversary-numbers` - список юбилейных номеров + * `anniversary-numbers-delta` - количество дополнительно отслеживаемых вступлений + * `chats-settings` - настройки групп. Настройки прописываются иерархически: к каждому ID группы модераторов прописывается список ID групп пользователей. + * `rewrite-chats-settings-in-database-on-startup` - перезапись настроек групп в базе данных на настройки из конфигурационного файла. + + + * Настройки шаблонов `message-templates`: + * `variables` - названия переменных шаблонов + * `plugs` - дополнительные моменты + * `join-congratulation` - шаблон сообщения поздравления пользователя + * `join-alert` - шаблон уведомления модераторов о юбилейном вступлении пользователя + * `join-user-info` - шаблон данных пользователя при использовании команды `/luckyList` + + +--- +## Примеры сообщений + +Уведомление модераторов + + 🎉 “Java разработчик” 👤 Василий (ника нет), + 🔢 500 🕐 26.06.22 10:56 + [ПОЗДРАВИТЬ] [ОТКЛОНИТЬ] + +Поздравление пользователя + + 🎉 Поздравляю, Никита, + как же удачно попали в нужное время и в нужное время! + Вы 500 участник коммьюнити. + Вас ждут плюшки и печенюшки!🎉 + +Вывод списка счастливчиков + + ================================================== + Группа: “Java разработчик” + ================================================== + 👑👑👑 👤Василий (ника нет), + 🔢 500 🕐 20.07.22 22:00 + + 👤GroupSkillboxBot (GroupSkillboxBot), + 🔢 501 🕐 20.07.22 23:06 + + 👑👑👑 👤Никита (nikita), + 🔢 1000 🕐 22.07.22 01:00 \ No newline at end of file diff --git a/application-bot.yml b/application-bot.yml new file mode 100644 index 0000000..a7723b7 --- /dev/null +++ b/application-bot.yml @@ -0,0 +1,36 @@ +chats-settings: + administrators: + - 1039061325 # pavel + - 161855902 # max + - 1004758635 # alex + anniversary-numbers-delta: 3 + anniversary-numbers: + - 5 + - 50 + - 100 + - 500 + - 777 + - 1000 + - 5000 + - 10000 + chats-settings: + -1001523814996: #Хакатон и телеграм бот (Skillbox) + - -1001523814996 # Хакатон и телеграм бот (Skillbox) + - -1001781082670 # Java-диплом с нуля (Skillbox) + + rewrite-chats-settings-in-database-on-startup: false + +message-templates: + variables: + chat-name: "{НазваниеГруппы}" + user-name: "{ИмяУчастника}" + user-nickname: "{НикУчастника}" + join-date: "{ВремяВступления}" + join-number: "{НомерВступления}" + plugs: + no-nick: "ника нет" + winner-crown: "👑👑👑" + join-date-format: "dd.MM.yy HH:mm" + join-congratulation: "🎉 Поздравляю, *{ИмяУчастника}*, как же удачно попали в нужное время и в нужное время!\nВы *{НомерВступления}* участник коммьюнити.\nВас ждут плюшки и печенюшки!🎉" + join-alert: " 🎉 В *{НазваниеГруппы}* группу вступил юбилейный пользователь\n{ИмяУчастника} (*{НикУчастника}*),\n 🔢 *{НомерВступления}* 🕐 {ВремяВступления}" + join-user-info: " 👤{ИмяУчастника} ({НикУчастника}),\n 🔢 *{НомерВступления}* 🕐 {ВремяВступления}" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e9c13ff --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +version: "3.9" +services: + postgres: + container_name: postgres + image: postgres:alpine + restart: always + ports: + - "5431:5432" + environment: + POSTGRES_DB: ${DATASOURCE_DB:-tg-bot-users} + POSTGRES_USER: ${DATASOURCE_USER:-tg-admin} + POSTGRES_PASSWORD: ${DATASOURCE_PASS:-postgres} + volumes: + - C:\docker\postgres:/var/lib/postgres + tg-bot-users: + container_name: tg-bot-users + image: tg-bot-users + restart: always + build: + context: . + ports: + - "8080:8080" + environment: + BOT_USER_NAME: ${BOT_USER_NAME} + BOT_TOKEN: ${BOT_TOKEN} + DATASOURCE_URL: ${DATASOURCE_URL:-jdbc:postgresql://host.docker.internal:5431/}${DATASOURCE_DB:-tg-bot-users} + DATASOURCE_USER: ${DATASOURCE_USER:-tg-admin} + DATASOURCE_PASS: ${DATASOURCE_PASS:-postgres} + depends_on: + postgres: + condition: service_started \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..8a8fb22 --- /dev/null +++ b/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# https://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..1d8ab01 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d1fd30b --- /dev/null +++ b/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.1 + + + + org.codewithoutus + tg-bot-users + 0.0.1-SNAPSHOT + + tg-bot-users + Demo project for Spring Boot + + + 17 + + + + + com.github.pengrad + java-telegram-bot-api + 6.1.0 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.postgresql + postgresql + runtime + + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + diff --git a/src/main/java/org/codewithoutus/tgbotusers/TgBotUsersApplication.java b/src/main/java/org/codewithoutus/tgbotusers/TgBotUsersApplication.java new file mode 100644 index 0000000..dace981 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/TgBotUsersApplication.java @@ -0,0 +1,14 @@ +package org.codewithoutus.tgbotusers; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; + +@SpringBootApplication +public class TgBotUsersApplication { + + public static void main(String[] args) { + ConfigurableApplicationContext context = SpringApplication.run(TgBotUsersApplication.class, args); + context.start(); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/Bot.java b/src/main/java/org/codewithoutus/tgbotusers/bot/Bot.java new file mode 100644 index 0000000..61daefa --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/Bot.java @@ -0,0 +1,22 @@ +package org.codewithoutus.tgbotusers.bot; + +import com.pengrad.telegrambot.TelegramBot; +import lombok.Getter; +import lombok.Setter; +import org.codewithoutus.tgbotusers.bot.enums.BotStatus; +import org.codewithoutus.tgbotusers.config.BotSettings; +import org.springframework.stereotype.Component; + +@Getter +@Setter +@Component +public class Bot extends TelegramBot { + + private final BotSettings botSettings; + private BotStatus status; + + public Bot(BotSettings botSettings) { + super(botSettings.getBotToken()); + this.botSettings = botSettings; + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/UpdateUtils.java b/src/main/java/org/codewithoutus/tgbotusers/bot/UpdateUtils.java new file mode 100644 index 0000000..f1cc36b --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/UpdateUtils.java @@ -0,0 +1,101 @@ +package org.codewithoutus.tgbotusers.bot; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.pengrad.telegrambot.model.*; +import lombok.extern.slf4j.Slf4j; +import org.codewithoutus.tgbotusers.bot.enums.BotCommand; +import org.codewithoutus.tgbotusers.bot.exception.CallbackDataMappingException; +import org.codewithoutus.tgbotusers.config.AppStaticContext; +import org.codewithoutus.tgbotusers.config.ChatSettings; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; + +@Slf4j +public class UpdateUtils { + + public static Boolean isPrivateMessage(Update update) { + return Optional.ofNullable(update.message()) + .map(Message::chat) + .map(chat -> chat.type() == Chat.Type.Private) + .orElse(Boolean.FALSE); + } + + public static boolean isPrivateMessageFromAdmin(Update update, ChatSettings chatSettings) { + return isPrivateMessage(update) && chatSettings.isAdminId(getUserId(update)); + } + + public static boolean isForwardMessage(Update update) { + return Optional.ofNullable(update.message()) + .map(Message::forwardDate) + .isPresent(); + } + + public static String getMessageText(Update update) { + return Optional.ofNullable(update.message()) + .map(Message::text) + .orElse(""); + } + + public static User[] getNewChatMembers(Update update) { + return Optional.ofNullable(update.message()) + .map(Message::newChatMembers) + .orElse(null); + } + + public static BotCommand getBotCommand(Update update) { + return Optional.ofNullable(update.message()) + .map(Message::entities) + .flatMap(entities -> Arrays.stream(entities) + .filter(entity -> entity.type().equals(MessageEntity.Type.bot_command)) + .findFirst() + .map(e -> getMessageText(update).substring(e.offset(), e.length())) + .map(BotCommand::getByCommandText)) + .orElse(null); + } + + public static Chat getChat(Update update) { + return Optional.ofNullable(update.message()) + .map(Message::chat) + .orElse(null); + } + + public static Long getChatId(Update update) { + Chat chat = getChat(update); + return chat == null ? null : chat.id(); + } + + public static User getUser(Update update) { + return Optional.ofNullable(update.message()) + .map(Message::from) + .orElse(null); + } + + public static Long getUserId(Update update) { + User user = getUser(update); + return user == null ? null : user.id(); + } + + public static String getCallbackQueryData(Update update) { + return Optional.ofNullable(update.callbackQuery()) + .map(CallbackQuery::data) + .filter(data -> !data.isBlank()) + .orElse("{}"); + } + + public static Map getCallbackQueryDataAsMap(Update update) { + return getCallbackQueryDataAsMap(getCallbackQueryData(update)); + } + + public static Map getCallbackQueryDataAsMap(String callbackData) { + try { + return AppStaticContext.OBJECT_MAPPER.readValue(callbackData, new TypeReference<>() { + }); + } catch (JsonProcessingException e) { + log.error("CallbackQuery json parsing to map error {}", callbackData); + throw new CallbackDataMappingException(callbackData); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/enums/BotCommand.java b/src/main/java/org/codewithoutus/tgbotusers/bot/enums/BotCommand.java new file mode 100644 index 0000000..ae5bb99 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/enums/BotCommand.java @@ -0,0 +1,49 @@ +package org.codewithoutus.tgbotusers.bot.enums; + +import lombok.Getter; + +import java.util.Arrays; +import java.util.regex.Pattern; + +@Getter +public enum BotCommand { + + CONGRATULATE("/congratulate", "T"), + DECLINE("/decline", "T"), + + LUCKY_LIST("/luckyList", "T( {id}})?"), + CHOOSE_LUCKY("/chooseLucky", "T( {id})?"), + + ADD_MODER_CHAT("/addModerChat", "T {id}"), + ADD_USER_CHAT("/addUserChat", "T {id}"), + DELETE_MODER_CHAT("/deleteModerChat", "T {id}"), + DELETE_USER_CHAT("/deleteUserChat", "T {id}"), + BIND_USER_CHAT_TO_MODER("/bindUserChatToModer", "T {id} {id}"), + UNBIND_USER_CHAT_FROM_MODER("/unbindUserChatFromModer", "T {id} {id}"), + + HELP("/help", "T"), + CURRENT_SETTINGS("/currentSettings", "T"); + + private static final String ID_REGEX = "(\\-?\\d*{18})"; + private final Pattern regex; + private final String text; + private final String params; + private final String help; + + BotCommand(String text, String regex) { + this.text = text; + this.params = regex.replace("T", ""); + this.help = regex.replace("T", text); + this.regex = Pattern.compile("^" + regex + .replace("T", text) + .replace("{id}", ID_REGEX) + + "$"); + } + + public static BotCommand getByCommandText(String command) { + return Arrays.stream(values()) + .filter(value -> command.startsWith(value.getText())) + .findFirst() + .orElse(null); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/enums/BotStatus.java b/src/main/java/org/codewithoutus/tgbotusers/bot/enums/BotStatus.java new file mode 100644 index 0000000..3b912d3 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/enums/BotStatus.java @@ -0,0 +1,6 @@ +package org.codewithoutus.tgbotusers.bot.enums; + +public enum BotStatus { + START, + STOP +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/exception/CallbackDataMappingException.java b/src/main/java/org/codewithoutus/tgbotusers/bot/exception/CallbackDataMappingException.java new file mode 100644 index 0000000..56d727c --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/exception/CallbackDataMappingException.java @@ -0,0 +1,8 @@ +package org.codewithoutus.tgbotusers.bot.exception; + +public class CallbackDataMappingException extends RuntimeException { + + public CallbackDataMappingException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/exception/CommandNotFoundException.java b/src/main/java/org/codewithoutus/tgbotusers/bot/exception/CommandNotFoundException.java new file mode 100644 index 0000000..f429f4e --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/exception/CommandNotFoundException.java @@ -0,0 +1,8 @@ +package org.codewithoutus.tgbotusers.bot.exception; + +public class CommandNotFoundException extends RuntimeException { + + public CommandNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/exception/TelegramSendMessageException.java b/src/main/java/org/codewithoutus/tgbotusers/bot/exception/TelegramSendMessageException.java new file mode 100644 index 0000000..7ae5575 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/exception/TelegramSendMessageException.java @@ -0,0 +1,8 @@ +package org.codewithoutus.tgbotusers.bot.exception; + +public class TelegramSendMessageException extends RuntimeException { + + public TelegramSendMessageException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/handler/AdminMessageHandler.java b/src/main/java/org/codewithoutus/tgbotusers/bot/handler/AdminMessageHandler.java new file mode 100644 index 0000000..e00054d --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/handler/AdminMessageHandler.java @@ -0,0 +1,284 @@ +package org.codewithoutus.tgbotusers.bot.handler; + +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.User; +import com.pengrad.telegrambot.model.request.ParseMode; +import com.pengrad.telegrambot.request.SendMessage; +import lombok.RequiredArgsConstructor; +import org.codewithoutus.tgbotusers.bot.UpdateUtils; +import org.codewithoutus.tgbotusers.bot.enums.BotCommand; +import org.codewithoutus.tgbotusers.bot.keyboard.AdminKeyboard; +import org.codewithoutus.tgbotusers.bot.keyboard.KeyboardUtils; +import org.codewithoutus.tgbotusers.bot.service.TelegramService; +import org.codewithoutus.tgbotusers.config.ChatSettings; +import org.codewithoutus.tgbotusers.model.entity.ChatModerator; +import org.codewithoutus.tgbotusers.model.entity.ChatUser; +import org.codewithoutus.tgbotusers.model.service.ChatModeratorService; +import org.codewithoutus.tgbotusers.model.service.ChatUserService; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; + +@Component +@RequiredArgsConstructor +public class AdminMessageHandler extends Handler { + + private static final String OK_ADD_MODER = "OK add moder chat № %s "; + private static final String OK_ADD_USER = "OK add user chat № %s"; + private static final String OK_BIND = "OK bind user chat № %s and moder chat № %s "; + private static final String OK_UNBIND = "OK unbind user chat № %s and moder chat № %s "; + private static final String OK_DEL_MODER = "OK delete moder chat № %s "; + private static final String OK_DEL_USER = "OK delete user chat № %s "; + + private static final String MODER_EXISTS = "Moder chat with id = %s exists"; + private static final String USER_EXISTS = "User chat with id = %s exists"; + private static final String MODER_NOT_FOUND = "Moder chat with id = %s not found"; + private static final String USER_NOT_FOUND = "User chat with id = %s not found"; + + private static final String FAIL_ADD_MODER = "FAIL add moder chat № %s "; + private static final String FAIL_ADD_USER = "FAIL add user chat № %s "; + private static final String ALREADY_BIND = "ALREADY bind user chat № %s and moder chat № %s "; + private static final String ALREADY_UNBIND = "ALREADY unbind user chat № %s and moder chat № %s "; + private static final String FAIL_DEL_MODER = "FAIL delete moder chat № %s "; + private static final String FAIL_DEL_USER = "FAIL delete user chat № %s "; + + + private final TelegramService telegramService; + private final ChatSettings chatSettings; + + private final ChatModeratorService chatModeratorService; + private final ChatUserService chatUserService; + + + @Override + protected boolean handle(Update update) { + User user = UpdateUtils.getUser(update); + if (user == null || !chatSettings.isAdminId(user.id()) || !UpdateUtils.isPrivateMessage(update)) { + return false; + } + + String text = UpdateUtils.getMessageText(update); + Long chatId = UpdateUtils.getChat(update).id(); + + AdminKeyboard command = KeyboardUtils.defineKey(AdminKeyboard.class, text).orElse(null); + if (command == null) { + return false; + } + return switch (command) { + case HELP -> showHelp(chatId); + case CURRENT_SETTINGS -> showCurrentSettings(chatId); + case ADD_MODER_CHAT -> addModerChat(chatId, text); + case ADD_USER_CHAT -> addUserChat(chatId, text); + case BIND_USER_CHAT_TO_MODER -> bindUserChatToModer(chatId, text); + case UNBIND_USER_CHAT_FROM_MODER -> unbindUserChatFromModer(chatId, text); + case DELETE_USER_CHAT -> deleteUserChat(chatId, text); + case DELETE_MODER_CHAT -> deleteModerChat(chatId, text); + default -> false; + }; + } + + private boolean showHelp(Long chatId) { + StringBuilder builder = new StringBuilder("Список доступных команд: ") + .append(System.lineSeparator()); + Arrays.stream(AdminKeyboard.values()) + .forEach(command -> { + String text = command.getBotCommand().getText(); + String params = command.getBotCommand().getParams(); + builder.append("").append(text).append("").append(params) + .append(System.lineSeparator()); + }); + telegramService.sendMessage(new SendMessage(chatId, builder.toString()).parseMode(ParseMode.HTML)); + return true; + } + + private boolean showCurrentSettings(Long chatId) { + StringBuilder settings = new StringBuilder("Текущие настройки групп:") + .append(System.lineSeparator()); + + List chatModerators = chatModeratorService.findAll(); + List chatUsers = chatUserService.findAll(); + + chatModerators.forEach(moderChat -> { + settings.append(" ").append(moderChat.getChatId()).append(':').append(System.lineSeparator()); + moderChat.getChatUsers().forEach(userChat -> { + settings.append(" ").append(userChat.getChatId()).append(System.lineSeparator()); + chatUsers.remove(userChat); + }); + }); + + if (!chatUsers.isEmpty()) { + settings.append("Не связанные группы:").append(System.lineSeparator()); + chatUsers.forEach(chatUser -> { + settings.append(" ").append(chatUser.getChatId()).append(System.lineSeparator()); + }); + } + + telegramService.sendMessage(new SendMessage(chatId, settings.toString())); + return true; + } + + + private boolean addModerChat(Long chatId, String text) { + Matcher matcher = BotCommand.ADD_MODER_CHAT.getRegex().matcher(text); + if (!matcher.matches()) { + return false; + } + + long moderatorGroupId = Long.parseLong(matcher.group(1)); + Optional chatModeratorOptional = chatModeratorService.findByChatId(moderatorGroupId); + if (chatModeratorOptional.isPresent()) { + telegramService.sendMessage(new SendMessage(chatId, String.format(MODER_EXISTS, moderatorGroupId))); + return true; + } + + Optional chatUserOptional = chatUserService.findByChatId(moderatorGroupId); + if (chatUserOptional.isPresent()) { + telegramService.sendMessage(new SendMessage(chatId, String.format(USER_EXISTS, moderatorGroupId))); + return true; + } + + ChatModerator newEntity = new ChatModerator(); + newEntity.setChatId(moderatorGroupId); + chatModeratorService.save(newEntity); + telegramService.sendMessage(new SendMessage(chatId, String.format(OK_ADD_MODER, moderatorGroupId))); + return true; + } + + private boolean addUserChat(Long chatId, String text) { + Matcher matcher = BotCommand.ADD_USER_CHAT.getRegex().matcher(text); + if (!matcher.matches()) { + return false; + } + + long userGroupId = Long.parseLong(matcher.group(1)); + Optional chatUserOptional = chatUserService.findByChatId(userGroupId); + if (chatUserOptional.isPresent()) { + telegramService.sendMessage(new SendMessage(chatId, String.format(USER_EXISTS, userGroupId))); + return true; + } + + Optional chatModeratorOptional = chatModeratorService.findByChatId(userGroupId); + if (chatModeratorOptional.isPresent()) { + telegramService.sendMessage(new SendMessage(chatId, String.format(MODER_EXISTS, userGroupId))); + return true; + } + + ChatUser newEntity = new ChatUser(); + newEntity.setChatId(userGroupId); + chatUserService.save(newEntity); + telegramService.sendMessage(new SendMessage(chatId, String.format(OK_ADD_USER, userGroupId))); + return true; + } + + private boolean bindUserChatToModer(Long chatId, String text) { + Matcher matcher = BotCommand.BIND_USER_CHAT_TO_MODER.getRegex().matcher(text); + if (!matcher.matches()) { + return false; + } + + long userGroupId = Long.parseLong(matcher.group(1)); + ChatUser chatUser = chatUserService.findByChatId(userGroupId).orElse(null); + if (chatUser == null) { + telegramService.sendMessage(new SendMessage(chatId, String.format(USER_NOT_FOUND, userGroupId))); + return true; + } + + long moderatorGroupId = Long.parseLong(matcher.group(2)); + ChatModerator chatModerator = chatModeratorService.findByChatId(moderatorGroupId).orElse(null); + if (chatModerator == null) { + telegramService.sendMessage(new SendMessage(chatId, String.format(MODER_NOT_FOUND, moderatorGroupId))); + return true; + } + + if (chatModerator.getChatUsers().contains(chatUser)) { + telegramService.sendMessage(new SendMessage(chatId, String.format(ALREADY_BIND, userGroupId, moderatorGroupId))); + return true; + } + + chatModerator.getChatUsers().add(chatUser); + chatModeratorService.save(chatModerator); + telegramService.sendMessage(new SendMessage(chatId, String.format(OK_BIND, userGroupId, moderatorGroupId))); + return true; + } + + + private boolean unbindUserChatFromModer(Long chatId, String text) { + Matcher matcher = BotCommand.UNBIND_USER_CHAT_FROM_MODER.getRegex().matcher(text); + if (!matcher.matches()) { + return false; + } + + long userGroupId = Long.parseLong(matcher.group(1)); + ChatUser chatUser = chatUserService.findByChatId(userGroupId).orElse(null); + if (chatUser == null) { + telegramService.sendMessage(new SendMessage(chatId, String.format(USER_NOT_FOUND, userGroupId))); + return true; + } + + long moderatorGroupId = Long.parseLong(matcher.group(2)); + ChatModerator chatModerator = chatModeratorService.findByChatId(moderatorGroupId).orElse(null); + if (chatModerator == null) { + telegramService.sendMessage(new SendMessage(chatId, String.format(MODER_NOT_FOUND, moderatorGroupId))); + return true; + } + + if (!chatModerator.getChatUsers().contains(chatUser)) { + telegramService.sendMessage(new SendMessage(chatId, String.format(ALREADY_UNBIND, userGroupId, moderatorGroupId))); + return true; + } + + chatModerator.getChatUsers().remove(chatUser); + chatModeratorService.save(chatModerator); + telegramService.sendMessage(new SendMessage(chatId, String.format(OK_UNBIND, userGroupId, moderatorGroupId))); + return true; + } + + private boolean deleteUserChat(Long chatId, String text) { + Matcher matcher = BotCommand.DELETE_USER_CHAT.getRegex().matcher(text); + if (!matcher.matches()) { + return false; + } + + long userGroupId = Long.parseLong(matcher.group(1)); + ChatUser chatUser = chatUserService.findByChatId(userGroupId).orElse(null); + if (chatUser == null) { + telegramService.sendMessage(new SendMessage(chatId, String.format(USER_NOT_FOUND, userGroupId))); + return true; + } + + if (!chatUser.getChatModerators().isEmpty()) { + telegramService.sendMessage(new SendMessage(chatId, String.format(FAIL_DEL_USER, userGroupId))); + return true; + } + + chatUserService.deleteById(chatUser.getId()); + telegramService.sendMessage(new SendMessage(chatId, String.format(OK_DEL_USER, userGroupId))); + return true; + } + + private boolean deleteModerChat(Long chatId, String text) { + Matcher matcher = BotCommand.DELETE_MODER_CHAT.getRegex().matcher(text); + if (!matcher.matches()) { + return false; + } + + long moderGroupId = Long.parseLong(matcher.group(1)); + ChatModerator chatModerator = chatModeratorService.findByChatId(moderGroupId).orElse(null); + if (chatModerator == null) { + telegramService.sendMessage(new SendMessage(chatId, String.format(USER_NOT_FOUND, moderGroupId))); + return true; + } + + if (!chatModerator.getChatUsers().isEmpty()) { + telegramService.sendMessage(new SendMessage(chatId, String.format(FAIL_DEL_MODER, moderGroupId))); + return true; + } + + chatModeratorService.deleteById(chatModerator.getId()); + telegramService.sendMessage(new SendMessage(chatId, String.format(OK_DEL_MODER, moderGroupId))); + return true; + } +} diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/handler/CallbackQueryHandler.java b/src/main/java/org/codewithoutus/tgbotusers/bot/handler/CallbackQueryHandler.java new file mode 100644 index 0000000..cd9fbc5 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/handler/CallbackQueryHandler.java @@ -0,0 +1,88 @@ +package org.codewithoutus.tgbotusers.bot.handler; + +import com.pengrad.telegrambot.model.Update; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.codewithoutus.tgbotusers.bot.UpdateUtils; +import org.codewithoutus.tgbotusers.bot.exception.CommandNotFoundException; +import org.codewithoutus.tgbotusers.bot.keyboard.CongratulationDecisionKeyboard; +import org.codewithoutus.tgbotusers.bot.keyboard.KeyboardUtils; +import org.codewithoutus.tgbotusers.bot.service.NotificationService; +import org.codewithoutus.tgbotusers.config.AppStaticContext; +import org.codewithoutus.tgbotusers.model.entity.UserJoining; +import org.codewithoutus.tgbotusers.model.enums.CongratulateStatus; +import org.codewithoutus.tgbotusers.model.service.UserJoiningService; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; + +@Component +@RequiredArgsConstructor +@Slf4j +public class CallbackQueryHandler extends Handler { + + private final NotificationService notificationService; + private final UserJoiningService userJoiningService; + + @Override + protected boolean handle(Update update) { + // есть ли callbackQuery в update + Map callbackQueryData = UpdateUtils.getCallbackQueryDataAsMap(update); + if (callbackQueryData == null || callbackQueryData.isEmpty()) { + return false; + } + + // есть ли команда в callbackQuery + String command = callbackQueryData.get("command"); + if (command == null || command.isBlank()) { + return false; + } + + if (handleCongratulationDecision(command, callbackQueryData)) { + return true; + } else { + log.error("Unhandled command: {}", callbackQueryData); + throw new CommandNotFoundException("Unhandled command: " + callbackQueryData); + } + } + + @Transactional + private boolean handleCongratulationDecision(String command, Map callbackQueryData) { + // есть ли команда в callbackQuery + CongratulationDecisionKeyboard decision = KeyboardUtils + .defineKey(CongratulationDecisionKeyboard.class, command).orElse(null); + if (decision == null) { + return false; + } + + // есть ли поздравленные в чате с таким порядковым номером + Integer userJoiningId = Integer.parseInt(callbackQueryData.get(AppStaticContext.CALLBACK_QUERY_DATA_ID_FIELD)); + UserJoining userJoining = userJoiningService.findById(userJoiningId) + .orElseThrow(() -> new IllegalStateException( + "CallbackQueryData with no exist user joining ID" + userJoiningId)); + + Long chatId = userJoining.getChatId(); + Long userId = userJoining.getUserId(); + Integer anniversaryNumber = userJoining.getAnniversaryNumber(); + + if (userJoiningService.existCongratulatedUser(chatId, anniversaryNumber)) { + return true; + } + + CongratulateStatus newStatus = (decision == CongratulationDecisionKeyboard.CONGRATULATE) + ? CongratulateStatus.CONGRATULATE + : CongratulateStatus.DECLINE; + userJoining.setStatus(newStatus); + userJoiningService.save(userJoining); + + if (decision == CongratulationDecisionKeyboard.CONGRATULATE) { + notificationService.deleteKeyboardFromAllJoiningNotifications(chatId, anniversaryNumber); + notificationService.notifyUserAboutAnniversaryJoining(userJoining); + + } else if (decision == CongratulationDecisionKeyboard.DECLINE) { + notificationService.deleteKeyboardFromJoiningNotification(userId, chatId, anniversaryNumber); + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/handler/ChatJoinMessageHandler.java b/src/main/java/org/codewithoutus/tgbotusers/bot/handler/ChatJoinMessageHandler.java new file mode 100644 index 0000000..ce2f55c --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/handler/ChatJoinMessageHandler.java @@ -0,0 +1,69 @@ +package org.codewithoutus.tgbotusers.bot.handler; + +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.User; +import lombok.RequiredArgsConstructor; +import org.codewithoutus.tgbotusers.bot.UpdateUtils; +import org.codewithoutus.tgbotusers.bot.service.NotificationService; +import org.codewithoutus.tgbotusers.bot.service.TelegramService; +import org.codewithoutus.tgbotusers.config.ChatSettings; +import org.codewithoutus.tgbotusers.model.entity.UserJoining; +import org.codewithoutus.tgbotusers.model.enums.CongratulateStatus; +import org.codewithoutus.tgbotusers.model.service.ChatUserService; +import org.codewithoutus.tgbotusers.model.service.UserJoiningService; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +@Component +@RequiredArgsConstructor +public class ChatJoinMessageHandler extends Handler { + + private final ChatSettings chatSettings; + private final ChatUserService chatUserService; + private final TelegramService telegramService; + private final NotificationService notificationService; + private final UserJoiningService userJoiningService; + + @Override + protected boolean handle(Update update) { + User[] users = UpdateUtils.getNewChatMembers(update); + if (users == null || users.length == 0) { + return false; + } + long chatId = UpdateUtils.getChat(update).id(); + if (!chatUserService.existByChatId(chatId)) { + return false; + } + + Instant instant = Instant.ofEpochSecond(update.message().date()); + LocalDateTime dateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + int chatNumbersCount = telegramService.getChatMembersCount(chatId); + + for (int i = 0; i < users.length; i++) { + User user = users[i]; + int joinNumber = chatNumbersCount - users.length + 1 + i; + int anniversaryNumber = chatSettings.getAnniversaryJoinNumber(chatId, joinNumber); + if (anniversaryNumber == 0 || userJoiningService.userWasAlreadyJoinedToChat(chatId, user.id())) { + return false; + } + + UserJoining userJoining = new UserJoining(); + userJoining.setChatId(chatId); + userJoining.setUserId(user.id()); + userJoining.setJoinTime(dateTime); + userJoining.setNumber(joinNumber); + userJoining.setAnniversaryNumber(anniversaryNumber); + userJoining.setStatus(CongratulateStatus.WAIT); + userJoining = userJoiningService.save(userJoining); + + // если не было поздравненных в чате с таким порядковым номером + if (!userJoiningService.existCongratulatedUser(chatId, anniversaryNumber)) { + notificationService.notifyModeratorsAboutUserJoining(userJoining); + } + } + return true; + } +} diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/handler/Handler.java b/src/main/java/org/codewithoutus/tgbotusers/bot/handler/Handler.java new file mode 100644 index 0000000..23fb737 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/handler/Handler.java @@ -0,0 +1,19 @@ +package org.codewithoutus.tgbotusers.bot.handler; + +import com.pengrad.telegrambot.model.Update; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class Handler { + + public boolean tryHandle(Update update) { + try { + return handle(update); + } catch (Exception ex) { + log.error(ex.getMessage()); + return false; + } + } + + abstract protected boolean handle(Update update); +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/handler/LuckyCommandHandler.java b/src/main/java/org/codewithoutus/tgbotusers/bot/handler/LuckyCommandHandler.java new file mode 100644 index 0000000..aec0036 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/handler/LuckyCommandHandler.java @@ -0,0 +1,152 @@ +package org.codewithoutus.tgbotusers.bot.handler; + +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; +import com.pengrad.telegrambot.model.request.ParseMode; +import com.pengrad.telegrambot.request.SendMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.codewithoutus.tgbotusers.bot.UpdateUtils; +import org.codewithoutus.tgbotusers.bot.enums.BotCommand; +import org.codewithoutus.tgbotusers.bot.keyboard.CongratulationDecisionKeyboard; +import org.codewithoutus.tgbotusers.bot.keyboard.KeyboardUtils; +import org.codewithoutus.tgbotusers.bot.service.NotificationService; +import org.codewithoutus.tgbotusers.bot.service.TelegramService; +import org.codewithoutus.tgbotusers.bot.service.TemplateEngine; +import org.codewithoutus.tgbotusers.config.ChatSettings; +import org.codewithoutus.tgbotusers.config.NotificationTemplates; +import org.codewithoutus.tgbotusers.model.entity.ChatModerator; +import org.codewithoutus.tgbotusers.model.entity.ChatUser; +import org.codewithoutus.tgbotusers.model.entity.UserJoining; +import org.codewithoutus.tgbotusers.model.service.ChatModeratorService; +import org.codewithoutus.tgbotusers.model.service.ChatUserService; +import org.codewithoutus.tgbotusers.model.service.UserJoiningService; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +@Slf4j +public class LuckyCommandHandler extends Handler { + + private final ChatSettings chatSettings; + private final NotificationTemplates notificationTemplates; + private final NotificationService notificationService; + private final TelegramService telegramService; + private final TemplateEngine templateEngine; + + private final ChatModeratorService chatModeratorService; + private final ChatUserService chatUserService; + private final UserJoiningService userJoiningService; + + @Override + protected boolean handle(Update update) { + BotCommand command = UpdateUtils.getBotCommand(update); + if (command != BotCommand.LUCKY_LIST && command != BotCommand.CHOOSE_LUCKY) { + return false; + } + + Long moderatorChatId = UpdateUtils.getChatId(update); + if (!UpdateUtils.isPrivateMessageFromAdmin(update, chatSettings) + && !chatModeratorService.existsByChatId(moderatorChatId)) { + return false; + } + + String commandText = UpdateUtils.getMessageText(update); + Matcher matcher = command.getRegex().matcher(commandText); + if (!matcher.matches()) { + log.warn("Wrong command to bot {}", commandText); + return false; + } + + switch (command) { + case LUCKY_LIST -> handleLuckyList(matcher, moderatorChatId); + case CHOOSE_LUCKY -> handleChooseLucky(matcher, moderatorChatId); + } + return true; + } + + private void handleChooseLucky(Matcher matcher, Long moderatorChatId) { + String chatId = matcher.group(2); + List luckyChatsIds = getLuckyChats(chatId, moderatorChatId) + .stream() + .map(ChatUser::getChatId) + .toList(); + + List luckyOnes = userJoiningService.findNotCongratulatedByChatIds(luckyChatsIds); + if (luckyOnes.isEmpty()) { + telegramService.sendMessage(new SendMessage(moderatorChatId, "Пока ещё нет счастливчиков")); + return; + } + + for (UserJoining userJoining : luckyOnes) { + InlineKeyboardMarkup keyboard = KeyboardUtils + .createKeyboard(CongratulationDecisionKeyboard.class, String.valueOf(userJoining.getId())); + + String notificationText = templateEngine + .buildFromTemplate(notificationTemplates.getJoinUserInfo(), userJoining, false); + + notificationService.sendModeratorNotification(moderatorChatId, userJoining, notificationText, keyboard); + } + } + + private void handleLuckyList(Matcher matcher, Long moderatorChatId) { + String chatId = matcher.group(2); + List luckyChats = getLuckyChats(chatId, moderatorChatId); + if (luckyChats.isEmpty()) { + telegramService.sendMessage(new SendMessage(moderatorChatId, "Нет данных о чатах пользователей")); + return; + } + + telegramService.sendMessage(new SendMessage(moderatorChatId, createLuckyListText(luckyChats)) + .parseMode(ParseMode.Markdown)); + } + + private List getLuckyChats(String chatId, Long moderatorChatId) { + if (chatId == null) { + return chatModeratorService + .findByChatId(moderatorChatId) + .map(ChatModerator::getChatUsers) + .orElseGet(ArrayList::new); + } else { + return chatUserService + .findByChatId(Long.parseLong(chatId)) + .stream() + .toList(); + } + } + + private String createLuckyListText(List luckyChats) { + List chatsIds = luckyChats.stream() + .map(ChatUser::getChatId) + .toList(); + + StringBuilder resultBuilder = new StringBuilder(); + String template = notificationTemplates.getJoinUserInfo(); + userJoiningService.findByChatIds(chatsIds) + .stream() + .collect(Collectors.groupingBy(x -> String.valueOf(x.getChatId()))) + .forEach((groupId, joiningList) -> { + resultBuilder + .append("==================================================").append(System.lineSeparator()) + .append("Группа: *") + .append(telegramService.getChat(Long.parseLong(groupId)).title()) + .append("*") + .append(System.lineSeparator()) + .append("==================================================").append(System.lineSeparator()); + joiningList.forEach(joining -> { + resultBuilder + .append(templateEngine.buildFromTemplate(template, joining, true)) + .append(System.lineSeparator()) + .append(System.lineSeparator()); + }); + resultBuilder.append(System.lineSeparator()); + }); + + return resultBuilder.isEmpty() ? "Пока ещё нет счастливчиков" : resultBuilder.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/handler/PrivateMessageHandler.java b/src/main/java/org/codewithoutus/tgbotusers/bot/handler/PrivateMessageHandler.java new file mode 100644 index 0000000..9d27eb3 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/handler/PrivateMessageHandler.java @@ -0,0 +1,65 @@ +package org.codewithoutus.tgbotusers.bot.handler; + +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.User; +import com.pengrad.telegrambot.model.request.InlineKeyboardButton; +import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; +import com.pengrad.telegrambot.request.SendMessage; +import lombok.RequiredArgsConstructor; +import org.codewithoutus.tgbotusers.bot.UpdateUtils; +import org.codewithoutus.tgbotusers.bot.service.TelegramService; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PrivateMessageHandler extends Handler { + + private final TelegramService telegramService; + private static final String SORRY = "Sorry, wrong or unknown command"; + + @Override + protected boolean handle(Update update) { + if (!UpdateUtils.isPrivateMessage(update)) { + return false; + } + + String text = UpdateUtils.getMessageText(update); + Long chatId = UpdateUtils.getChat(update).id(); + + if (handleForwardMessage(chatId, update) || handleTestRequest(chatId, text)) { + return true; + } + telegramService.sendMessage(new SendMessage(chatId, SORRY)); + return false; + } + + private boolean handleForwardMessage(Long chatId, Update update) { + if (!UpdateUtils.isForwardMessage(update)) { + return false; + } + Integer messageId = update.message().messageId(); + User user = telegramService.getUser(chatId, chatId); + String userName = user.firstName().isBlank() ? user.username() : user.firstName(); + String text = userName + ", сплетничать не хорошо"; + telegramService.sendMessage(new SendMessage(chatId, text).replyToMessageId(messageId)); + return true; + } + + private boolean handleTestRequest(Long chatId, String text) { + if (text.startsWith("/test ")) { + telegramService.sendMessage(new SendMessage(chatId, "Идёт тест...")); + if (text.startsWith("/test InlineKeyboard")) { + String callbackData = text.substring("/test InlineKeyboard".length() + 1).trim(); + if (!callbackData.isBlank()) { + InlineKeyboardMarkup keyboard = new InlineKeyboardMarkup( + new InlineKeyboardButton("callback_data").callbackData(callbackData)); + telegramService.sendMessage(new SendMessage(chatId, "InlineKeyboard test").replyMarkup(keyboard)); + } + } else { + telegramService.sendMessage(new SendMessage(chatId, SORRY)); + } + return true; + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/keyboard/AdminKeyboard.java b/src/main/java/org/codewithoutus/tgbotusers/bot/keyboard/AdminKeyboard.java new file mode 100644 index 0000000..f837344 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/keyboard/AdminKeyboard.java @@ -0,0 +1,28 @@ +package org.codewithoutus.tgbotusers.bot.keyboard; + +import lombok.Getter; +import org.codewithoutus.tgbotusers.bot.enums.BotCommand; + +@Getter +public enum AdminKeyboard implements Keyboard { + + HELP(BotCommand.HELP, "Помощь"), + CURRENT_SETTINGS(BotCommand.CURRENT_SETTINGS, "Текущие настройки"), + + ADD_MODER_CHAT(BotCommand.ADD_MODER_CHAT, "Добавить чат модераторов"), + ADD_USER_CHAT(BotCommand.ADD_USER_CHAT, "Добавить чат пользователей"), + + DELETE_MODER_CHAT(BotCommand.DELETE_MODER_CHAT, "Удалить чат модераторов"), + DELETE_USER_CHAT(BotCommand.DELETE_USER_CHAT, "Удалить чат пользователей"), + + BIND_USER_CHAT_TO_MODER(BotCommand.BIND_USER_CHAT_TO_MODER, "Привязать чат пользователей к модераторам"), + UNBIND_USER_CHAT_FROM_MODER(BotCommand.UNBIND_USER_CHAT_FROM_MODER, "Отвязать чат пользователей к модераторам"); + + private final BotCommand botCommand; + private final String representation; + + AdminKeyboard(BotCommand botCommand, String representation) { + this.botCommand = botCommand; + this.representation = representation; + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/keyboard/CongratulationDecisionKeyboard.java b/src/main/java/org/codewithoutus/tgbotusers/bot/keyboard/CongratulationDecisionKeyboard.java new file mode 100644 index 0000000..2e7a96f --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/keyboard/CongratulationDecisionKeyboard.java @@ -0,0 +1,19 @@ +package org.codewithoutus.tgbotusers.bot.keyboard; + +import lombok.Getter; +import org.codewithoutus.tgbotusers.bot.enums.BotCommand; + +@Getter +public enum CongratulationDecisionKeyboard implements Keyboard { + + CONGRATULATE(BotCommand.CONGRATULATE, "Поздравить \uD83E\uDD73"), + DECLINE(BotCommand.DECLINE, "Отклонить 🚫"); + + private final BotCommand botCommand; + private final String representation; + + CongratulationDecisionKeyboard(BotCommand botCommand, String representation) { + this.botCommand = botCommand; + this.representation = representation; + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/keyboard/Keyboard.java b/src/main/java/org/codewithoutus/tgbotusers/bot/keyboard/Keyboard.java new file mode 100644 index 0000000..deb17c5 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/keyboard/Keyboard.java @@ -0,0 +1,9 @@ +package org.codewithoutus.tgbotusers.bot.keyboard; + +import org.codewithoutus.tgbotusers.bot.enums.BotCommand; + +public interface Keyboard { + + BotCommand getBotCommand(); + String getRepresentation(); +} diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/keyboard/KeyboardUtils.java b/src/main/java/org/codewithoutus/tgbotusers/bot/keyboard/KeyboardUtils.java new file mode 100644 index 0000000..8e842d9 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/keyboard/KeyboardUtils.java @@ -0,0 +1,29 @@ +package org.codewithoutus.tgbotusers.bot.keyboard; + +import com.pengrad.telegrambot.model.request.InlineKeyboardButton; +import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; +import org.codewithoutus.tgbotusers.config.AppStaticContext; + +import java.util.Arrays; +import java.util.Optional; + +public class KeyboardUtils { + + public static InlineKeyboardMarkup createKeyboard(Class enumClass, String callbackDataId) { + return new InlineKeyboardMarkup(Arrays + .stream(enumClass.getEnumConstants()) + .map(value -> new InlineKeyboardButton(value.getRepresentation()).callbackData( + AppStaticContext.OBJECT_MAPPER + .createObjectNode() + .put("command", value.getBotCommand().getText()) + .put(AppStaticContext.CALLBACK_QUERY_DATA_ID_FIELD, callbackDataId) + .toString())) + .toArray(InlineKeyboardButton[]::new)); + } + + public static Optional defineKey(Class enumClass, String command) { + return Arrays.stream(enumClass.getEnumConstants()) + .filter(value -> command.startsWith(value.getBotCommand().getText())) + .findFirst(); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/service/BotService.java b/src/main/java/org/codewithoutus/tgbotusers/bot/service/BotService.java new file mode 100644 index 0000000..7c54265 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/service/BotService.java @@ -0,0 +1,71 @@ +package org.codewithoutus.tgbotusers.bot.service; + +import com.pengrad.telegrambot.UpdatesListener; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.request.GetUpdates; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.codewithoutus.tgbotusers.bot.Bot; +import org.codewithoutus.tgbotusers.bot.enums.BotStatus; +import org.codewithoutus.tgbotusers.bot.handler.*; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class BotService { + + private final Bot bot; + private final AdminMessageHandler adminMessageHandler; + private final CallbackQueryHandler callbackQueryHandler; + private final ChatJoinMessageHandler chatJoinMessageHandler; + private final LuckyCommandHandler luckyListCommandHandler; + private final PrivateMessageHandler privateMessageHandler; + + public boolean start() { + if (bot.getStatus() == BotStatus.START) { + return false; + } + startUpdatePolling(); + bot.setStatus(BotStatus.START); + log.info("Bot running!"); + return true; + } + + public boolean stop() { + if (bot.getStatus() == BotStatus.STOP) { + return false; + } + stopUpdatePolling(); + bot.setStatus(BotStatus.STOP); + log.info("Bot stopped!"); + return true; + } + + public BotStatus getStatus() { + return bot.getStatus(); + } + + private void startUpdatePolling() { + GetUpdates timeout = new GetUpdates().timeout(bot.getBotSettings().getLongPollingTimeout()); + UpdatesListener updatesListener = (updates -> { + updates.forEach(this::handleUpdate); + return UpdatesListener.CONFIRMED_UPDATES_ALL; + }); + + bot.setUpdatesListener(updatesListener, timeout); + } + + private void stopUpdatePolling() { + bot.removeGetUpdatesListener(); + } + + private void handleUpdate(Update update) { + if (adminMessageHandler.tryHandle(update) + || privateMessageHandler.tryHandle(update) + || chatJoinMessageHandler.tryHandle(update) + || callbackQueryHandler.tryHandle(update) + || luckyListCommandHandler.tryHandle(update)) { + } + } +} diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/service/NotificationService.java b/src/main/java/org/codewithoutus/tgbotusers/bot/service/NotificationService.java new file mode 100644 index 0000000..dbc5d45 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/service/NotificationService.java @@ -0,0 +1,107 @@ +package org.codewithoutus.tgbotusers.bot.service; + +import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; +import com.pengrad.telegrambot.model.request.ParseMode; +import com.pengrad.telegrambot.request.SendMessage; +import com.pengrad.telegrambot.response.SendResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.codewithoutus.tgbotusers.bot.keyboard.CongratulationDecisionKeyboard; +import org.codewithoutus.tgbotusers.bot.keyboard.KeyboardUtils; +import org.codewithoutus.tgbotusers.config.NotificationTemplates; +import org.codewithoutus.tgbotusers.model.entity.ChatModerator; +import org.codewithoutus.tgbotusers.model.entity.UserJoining; +import org.codewithoutus.tgbotusers.model.entity.UserJoiningNotification; +import org.codewithoutus.tgbotusers.model.service.ChatModeratorService; +import org.codewithoutus.tgbotusers.model.service.UserJoiningNotificationService; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class NotificationService { + + private final NotificationTemplates notificationTemplates; + private final TemplateEngine templateEngine; + private final TelegramService telegramService; + private final ChatModeratorService chatModeratorService; + private final UserJoiningNotificationService userJoiningNotificationService; + + @Transactional + public void notifyModeratorsAboutUserJoining(UserJoining userJoining) { + log.debug("Start notifying about user(id={}, number={}) joining", userJoining.getUserId(), userJoining.getNumber()); + List moderatorChats = chatModeratorService.findByChatUsersId(userJoining.getChatId()); + if (moderatorChats.isEmpty()) { + log.warn("No binding moderators to user chat {}", userJoining.getChatId()); + return; + } + + InlineKeyboardMarkup keyboard = KeyboardUtils + .createKeyboard(CongratulationDecisionKeyboard.class, String.valueOf(userJoining.getId())); + + String notificationText = templateEngine + .buildFromTemplate(notificationTemplates.getJoinAlert(), userJoining, false); + + for (ChatModerator moderatorChat : moderatorChats) { + sendModeratorNotification(moderatorChat.getChatId(), userJoining, notificationText, keyboard); + } + log.debug("Finish notifying about user(id={}) joining", userJoining.getUserId()); + } + + @Transactional + public void sendModeratorNotification(Long moderatorChatId, UserJoining userJoining, String notificationText, InlineKeyboardMarkup keyboard) { + SendMessage message = new SendMessage(moderatorChatId, notificationText) + .replyMarkup(keyboard) + .parseMode(ParseMode.Markdown); + SendResponse response = telegramService.sendMessage(message); + log.debug("ModerChat(id={}) notified about user(id={}) joining. Status={}", + moderatorChatId, userJoining.getUserId(), response.isOk()); + + UserJoiningNotification notification = new UserJoiningNotification(); + notification.setUserJoining(userJoining); + notification.setSentMessageChatId(response.message().chat().id()); + notification.setSentMessageId(response.message().messageId()); + notification.setHasKeyboard(true); + userJoiningNotificationService.save(notification); + log.debug("Saved to DB notifying ModerChat(id={}) about user(id={}) joining", + moderatorChatId, userJoining.getUserId()); + } + + @Transactional + public void notifyUserAboutAnniversaryJoining(UserJoining userJoining) { + log.debug("Start congratulate user(id={}, number={}) joining", userJoining.getUserId(), userJoining.getNumber()); + String notificationText = templateEngine + .buildFromTemplate(notificationTemplates.getJoinCongratulation(), userJoining, false); + + SendMessage message = new SendMessage(userJoining.getChatId(), notificationText).parseMode(ParseMode.Markdown); + SendResponse response = telegramService.sendMessage(message); + log.debug("Finish congratulate user(id={}). Status={}", userJoining.getUserId(), response.isOk()); + } + + @Transactional + public void deleteKeyboardFromJoiningNotification(Long userId, Long chatId, int anniversaryNumber) { + log.debug("Start keyboard deleting about user(id={}, number={}) joining", userId, anniversaryNumber); + userJoiningNotificationService + .findByChatIdAndUserIdAndKeyboardStatus(chatId, userId, true) + .forEach(this::deleteKeyboard); + log.debug("Finish keyboard deleting about user(id={}, number={}) joining", userId, anniversaryNumber); + } + + @Transactional + public void deleteKeyboardFromAllJoiningNotifications(Long chatId, int anniversaryNumber) { + log.debug("Start keyboard deleting about chat(id={}) {} joining", chatId, anniversaryNumber); + userJoiningNotificationService + .findByChatIdAndAnniversaryNumberAndKeyboardStatus(chatId, anniversaryNumber, true) + .forEach(this::deleteKeyboard); + log.debug("Finish keyboard deleting about chat(id={}) {} joining", chatId, anniversaryNumber); + } + + @Transactional + private void deleteKeyboard(UserJoiningNotification notification) { + telegramService.removeKeyboardFromMessage(notification.getSentMessageChatId(), notification.getSentMessageId()); + notification.setHasKeyboard(false); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/service/TelegramService.java b/src/main/java/org/codewithoutus/tgbotusers/bot/service/TelegramService.java new file mode 100644 index 0000000..c9a0fc3 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/service/TelegramService.java @@ -0,0 +1,70 @@ +package org.codewithoutus.tgbotusers.bot.service; + +import com.pengrad.telegrambot.model.Chat; +import com.pengrad.telegrambot.model.User; +import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; +import com.pengrad.telegrambot.request.*; +import com.pengrad.telegrambot.response.BaseResponse; +import com.pengrad.telegrambot.response.SendResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.codewithoutus.tgbotusers.bot.Bot; +import org.codewithoutus.tgbotusers.bot.exception.TelegramSendMessageException; +import org.codewithoutus.tgbotusers.config.AppStaticContext; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class TelegramService { + + private final Bot bot; + + private , R extends BaseResponse> R sendRequest(BaseRequest request) { + R response = bot.execute(request); + if (!response.isOk()) { + String requestAsString; + try { + requestAsString = AppStaticContext.OBJECT_MAPPER.createObjectNode() + .put("parameters", request.getParameters().toString()) + .toString(); + + } catch (Exception ex) { + requestAsString = request.toString(); + } + log.error("Request {} -- get error response {}", requestAsString, response); + throw new TelegramSendMessageException(requestAsString); + } + return response; + } + + public SendResponse sendMessage(SendMessage message) { + SendResponse response = sendRequest(message); + log.debug("Sent message text=''{}''. Status={}", message.getParameters().get("text"), response.isOk()); + return response; + } + + public BaseResponse removeKeyboardFromMessage(long chatId, int messageId) { + BaseResponse response = sendRequest(new EditMessageReplyMarkup(chatId, messageId).replyMarkup(new InlineKeyboardMarkup())); + log.debug("Keyboard removed from message(id={}) in chat(id={}). Status={}", messageId, chatId, response.isOk()); + return response; + } + + public User getUser(long chatId, long userId) { + User user = sendRequest(new GetChatMember(chatId, userId)).chatMember().user(); + log.debug("Receive getUser: user(id={}, chatId={})", userId, chatId); + return user; + } + + public Chat getChat(long chatId) { + Chat chat = sendRequest(new GetChat(chatId)).chat(); + log.debug("Receive getChat: chat(id={})", chatId); + return chat; + } + + public int getChatMembersCount(long chatId) { + int count = sendRequest(new GetChatMemberCount(chatId)).count(); + log.debug("Receive getChatMembersCount: count={} for chat(id={})", count, chatId); + return count; + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/bot/service/TemplateEngine.java b/src/main/java/org/codewithoutus/tgbotusers/bot/service/TemplateEngine.java new file mode 100644 index 0000000..08b4bb2 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/bot/service/TemplateEngine.java @@ -0,0 +1,89 @@ +package org.codewithoutus.tgbotusers.bot.service; + +import com.pengrad.telegrambot.model.Chat; +import com.pengrad.telegrambot.model.User; +import lombok.RequiredArgsConstructor; +import org.codewithoutus.tgbotusers.config.NotificationTemplates; +import org.codewithoutus.tgbotusers.model.entity.UserJoining; +import org.codewithoutus.tgbotusers.model.enums.CongratulateStatus; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +@RequiredArgsConstructor +public class TemplateEngine { + + private final NotificationTemplates notificationTemplates; + private final TelegramService telegramService; + + public String buildFromTemplate(String template, UserJoining userJoining, boolean showCrown) { + Chat chat = telegramService.getChat(userJoining.getChatId()); + User user = telegramService.getUser(userJoining.getChatId(), userJoining.getUserId()); + + return buildFromTemplate(template) + .withJoinDate(userJoining.getJoinTime()) + .withJoinNumber(userJoining.getAnniversaryNumber()) + .withUserName(user) + .withUserNickname(user) + .withChatName(chat) + .withWinnerCrown(showCrown && userJoining.getStatus() == CongratulateStatus.CONGRATULATE) + .done(); + } + + public Builder buildFromTemplate(String template) { + return new Builder(template, notificationTemplates); + } + + public static class Builder { + private final StringBuilder result; + private final NotificationTemplates temp; + + private Builder(String template, NotificationTemplates templates) { + result = new StringBuilder(template); + this.temp = templates; + } + + private void replace(String target, String replacement) { + for (int i = result.lastIndexOf(target); i > -1; i = result.lastIndexOf(target, i)) { + result.replace(i, i + target.length(), replacement); + } + } + + public Builder withChatName(Chat chat) { + replace(temp.getVariables().get("chat-name"), chat.title()); + return this; + } + + public Builder withUserName(User user) { + String name = user.firstName() + (user.lastName() == null ? "" : (" " + user.lastName())); + replace(temp.getVariables().get("user-name"), name); + return this; + } + + public Builder withUserNickname(User user) { + String nickname = user.username() == null ? temp.getPlugs().get("no-nick") : user.username(); + replace(temp.getVariables().get("user-nickname"), nickname); + return this; + } + + public Builder withJoinDate(LocalDateTime dateTime) { + replace(temp.getVariables().get("join-date"), temp.getDateTimeFormatter().format(dateTime)); + return this; + } + + public Builder withJoinNumber(int number) { + replace(temp.getVariables().get("join-number"), String.valueOf(number)); + return this; + } + + public Builder withWinnerCrown(boolean isWinner) { + result.insert(0, isWinner ? temp.getPlugs().get("winner-crown") : ""); + return this; + } + + public String done() { + return result.toString(); + } + } +} diff --git a/src/main/java/org/codewithoutus/tgbotusers/config/AppListener.java b/src/main/java/org/codewithoutus/tgbotusers/config/AppListener.java new file mode 100644 index 0000000..11ad9a7 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/config/AppListener.java @@ -0,0 +1,21 @@ +package org.codewithoutus.tgbotusers.config; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.codewithoutus.tgbotusers.bot.service.BotService; +import org.springframework.context.event.ContextStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class AppListener { + + private final BotService botService; + + @EventListener + public void handleContextStartedEvent(ContextStartedEvent ctxStartEvt) { + botService.start(); + } +} diff --git a/src/main/java/org/codewithoutus/tgbotusers/config/AppStaticContext.java b/src/main/java/org/codewithoutus/tgbotusers/config/AppStaticContext.java new file mode 100644 index 0000000..667e92e --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/config/AppStaticContext.java @@ -0,0 +1,9 @@ +package org.codewithoutus.tgbotusers.config; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class AppStaticContext { + + public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + public static final String CALLBACK_QUERY_DATA_ID_FIELD = "dataId"; +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/config/BotSettings.java b/src/main/java/org/codewithoutus/tgbotusers/config/BotSettings.java new file mode 100644 index 0000000..af6828f --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/config/BotSettings.java @@ -0,0 +1,17 @@ +package org.codewithoutus.tgbotusers.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "bot-settings") +public class BotSettings { + + private int longPollingTimeout; + private String botToken; + private String botUserName; +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/config/ChatSettings.java b/src/main/java/org/codewithoutus/tgbotusers/config/ChatSettings.java new file mode 100644 index 0000000..2582909 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/config/ChatSettings.java @@ -0,0 +1,89 @@ +package org.codewithoutus.tgbotusers.config; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.codewithoutus.tgbotusers.model.entity.ChatModerator; +import org.codewithoutus.tgbotusers.model.entity.ChatUser; +import org.codewithoutus.tgbotusers.model.service.ChatModeratorService; +import org.codewithoutus.tgbotusers.model.service.ChatUserService; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; +import javax.transaction.Transactional; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "chats-settings") +@RequiredArgsConstructor +@Slf4j +public class ChatSettings { + + private final ChatModeratorService chatModeratorService; + private final ChatUserService chatUserService; + + private Set administrators; + private Integer anniversaryNumbersDelta; + private Set anniversaryNumbers; + + @Getter(AccessLevel.NONE) + private Map> chatsSettings; // only used for loading from application-settings file + private Boolean rewriteChatsSettingsInDatabaseOnStartup; + + public boolean isAdminId(Long id) { + return id != null && administrators.contains(id); + } + + public int getAnniversaryJoinNumber(long chatId, int joinNumber) { + return anniversaryNumbers.stream() + .filter(anniversaryNumber -> joinNumber >= anniversaryNumber) + .filter(anniversaryNumber -> joinNumber <= anniversaryNumber + anniversaryNumbersDelta) + .findFirst() + .orElse(0); + } + + @PostConstruct + private void synchronizeDataBaseSettings() { + if (rewriteChatsSettingsInDatabaseOnStartup || chatModeratorService.findAll().isEmpty()) { + rewriteDataBaseSettings(); + } + } + + @Transactional + private void rewriteDataBaseSettings() { + chatModeratorService.deleteAll(); + chatUserService.deleteAll(); + + // TODO : Павел - необходимо: + // 1. при запуске приложения проверять настройки в базе, + // если в базе пусто проверять конфигурацию и записывать в базу + // 2. при каждом запуске запуске актуализировать названия юзерских чатов (поле name) + // (нужно для возможности получения списка юбилейных с параметром названия группы, + // пример команды: [/luckyList@UsersTgBot Java разработчик]) + + for (Map.Entry> moderatorData : chatsSettings.entrySet()) { + ChatModerator chatModerator = new ChatModerator(); + chatModerator.setChatId(moderatorData.getKey()); + + for (Long userData : moderatorData.getValue()) { + ChatUser chatUser = chatUserService.findByChatId(userData).orElseGet(() -> { + ChatUser newEntity = new ChatUser(); + newEntity.setChatId(userData); + newEntity = chatUserService.save(newEntity); + return newEntity; + }); + chatUser.getChatModerators().add(chatModerator); + chatModerator.getChatUsers().add(chatUser); + chatModerator = chatModeratorService.save(chatModerator); + } + } + log.info("Group settings was written to DB"); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/config/NotificationTemplates.java b/src/main/java/org/codewithoutus/tgbotusers/config/NotificationTemplates.java new file mode 100644 index 0000000..464ec60 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/config/NotificationTemplates.java @@ -0,0 +1,30 @@ +package org.codewithoutus.tgbotusers.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; +import java.time.format.DateTimeFormatter; +import java.util.Map; + +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "message-templates") +public class NotificationTemplates { + + private Map variables; + private Map plugs; + private DateTimeFormatter dateTimeFormatter; + + private String joinCongratulation; + private String joinAlert; + private String joinUserInfo; + + @PostConstruct + private void postConstruct() { + dateTimeFormatter = DateTimeFormatter.ofPattern(plugs.get("join-date-format")); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/controller/BotController.java b/src/main/java/org/codewithoutus/tgbotusers/controller/BotController.java new file mode 100644 index 0000000..c652baf --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/controller/BotController.java @@ -0,0 +1,41 @@ +package org.codewithoutus.tgbotusers.controller; + +import com.pengrad.telegrambot.request.SendMessage; +import lombok.RequiredArgsConstructor; +import org.codewithoutus.tgbotusers.bot.service.BotService; +import org.codewithoutus.tgbotusers.bot.service.TelegramService; +import org.codewithoutus.tgbotusers.controller.dto.BotResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("c") +@RequiredArgsConstructor +public class BotController { + + private final BotService botService; + private final TelegramService telegramService; + + @GetMapping("/start") + private BotResponse startBotBackend() { + return new BotResponse(botService.start(), botService.getStatus()); + } + + @GetMapping("/stop") + private BotResponse stopBotBackend() { + return new BotResponse(botService.stop(), botService.getStatus()); + } + + @GetMapping("/status") + private BotResponse getStatus() { + return new BotResponse(true, botService.getStatus()); + } + + @GetMapping("/sendMessage") + private BotResponse sendMessage(@RequestParam Long chatId, @RequestParam String message) { + telegramService.sendMessage(new SendMessage(chatId, message)); + return new BotResponse(true, botService.getStatus()); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/controller/dto/BotResponse.java b/src/main/java/org/codewithoutus/tgbotusers/controller/dto/BotResponse.java new file mode 100644 index 0000000..cc9630e --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/controller/dto/BotResponse.java @@ -0,0 +1,10 @@ +package org.codewithoutus.tgbotusers.controller.dto; + +import lombok.Data; +import org.codewithoutus.tgbotusers.bot.enums.BotStatus; + +@Data +public final class BotResponse { + private final boolean ok; + private final BotStatus status; +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/model/entity/ChatModerator.java b/src/main/java/org/codewithoutus/tgbotusers/model/entity/ChatModerator.java new file mode 100644 index 0000000..8d399d5 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/model/entity/ChatModerator.java @@ -0,0 +1,43 @@ +package org.codewithoutus.tgbotusers.model.entity; + +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.NaturalId; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Entity +@Getter +@Setter +public class ChatModerator { // TODO: Pavel -- переименовать сущность и сопутствующие переменные (после задачи Макса) + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @NaturalId + @Column(nullable = false, unique = true) + private Long chatId; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "moderators2users", + joinColumns = @JoinColumn(name = "moderator_chat_id"), + inverseJoinColumns = @JoinColumn(name = "user_chat_id")) + private List chatUsers = new ArrayList<>(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ChatModerator that = (ChatModerator) o; + return chatId.equals(that.chatId); + } + + @Override + public int hashCode() { + return Objects.hash(chatId); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/model/entity/ChatUser.java b/src/main/java/org/codewithoutus/tgbotusers/model/entity/ChatUser.java new file mode 100644 index 0000000..3622522 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/model/entity/ChatUser.java @@ -0,0 +1,40 @@ +package org.codewithoutus.tgbotusers.model.entity; + +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.NaturalId; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Entity +@Getter +@Setter +public class ChatUser { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @NaturalId + @Column(nullable = false, unique = true) + private Long chatId; + + @ManyToMany(mappedBy = "chatUsers", fetch = FetchType.EAGER, cascade = CascadeType.MERGE) + private List chatModerators = new ArrayList<>(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ChatUser that = (ChatUser) o; + return chatId.equals(that.chatId); + } + + @Override + public int hashCode() { + return Objects.hash(chatId); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/model/entity/UserJoining.java b/src/main/java/org/codewithoutus/tgbotusers/model/entity/UserJoining.java new file mode 100644 index 0000000..5cbc826 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/model/entity/UserJoining.java @@ -0,0 +1,59 @@ +package org.codewithoutus.tgbotusers.model.entity; + +import lombok.Getter; +import lombok.Setter; +import org.codewithoutus.tgbotusers.model.enums.CongratulateStatus; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@Getter +@Setter +public class UserJoining { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(nullable = false) + private Long userId; + + @Column(nullable = false) + private Long chatId; + + @Column(nullable = false) + private Integer number; + + @Column(nullable = false) + private Integer anniversaryNumber; + + @Column(nullable = false) + private LocalDateTime joinTime; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private CongratulateStatus status; + + @OneToMany(mappedBy = "userJoining") + private List notifications; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UserJoining that = (UserJoining) o; + + if (!userId.equals(that.userId)) return false; + return chatId.equals(that.chatId); + } + + @Override + public int hashCode() { + int result = userId.hashCode(); + result = 31 * result + chatId.hashCode(); + return result; + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/model/entity/UserJoiningNotification.java b/src/main/java/org/codewithoutus/tgbotusers/model/entity/UserJoiningNotification.java new file mode 100644 index 0000000..9b22100 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/model/entity/UserJoiningNotification.java @@ -0,0 +1,28 @@ +package org.codewithoutus.tgbotusers.model.entity; + +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.*; + +@Entity +@Getter +@Setter +public class UserJoiningNotification { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(nullable = false) + private Integer sentMessageId; + + @Column(nullable = false) + private Long sentMessageChatId; + + @Column + private boolean hasKeyboard; + + @ManyToOne + private UserJoining userJoining; +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/model/enums/CongratulateStatus.java b/src/main/java/org/codewithoutus/tgbotusers/model/enums/CongratulateStatus.java new file mode 100644 index 0000000..05f7e16 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/model/enums/CongratulateStatus.java @@ -0,0 +1,7 @@ +package org.codewithoutus.tgbotusers.model.enums; + +public enum CongratulateStatus { + WAIT, + CONGRATULATE, + DECLINE +} diff --git a/src/main/java/org/codewithoutus/tgbotusers/model/repository/ChatModeratorRepository.java b/src/main/java/org/codewithoutus/tgbotusers/model/repository/ChatModeratorRepository.java new file mode 100644 index 0000000..98bc665 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/model/repository/ChatModeratorRepository.java @@ -0,0 +1,18 @@ +package org.codewithoutus.tgbotusers.model.repository; + +import org.codewithoutus.tgbotusers.model.entity.ChatModerator; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ChatModeratorRepository extends CrudRepository { + + OptionalfindByChatId(Long chatId); + + List findByChatUsers_ChatId(Long chatId); + + boolean existsByChatId(Long chatId); +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/model/repository/ChatUserRepository.java b/src/main/java/org/codewithoutus/tgbotusers/model/repository/ChatUserRepository.java new file mode 100644 index 0000000..3dfc7e5 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/model/repository/ChatUserRepository.java @@ -0,0 +1,16 @@ +package org.codewithoutus.tgbotusers.model.repository; + +import org.codewithoutus.tgbotusers.model.entity.ChatUser; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ChatUserRepository extends CrudRepository { + + Optional findByChatId(long chatId); + + boolean existsByChatId(Long chatId); +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/model/repository/UserJoiningNotificationRepository.java b/src/main/java/org/codewithoutus/tgbotusers/model/repository/UserJoiningNotificationRepository.java new file mode 100644 index 0000000..ce32a3a --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/model/repository/UserJoiningNotificationRepository.java @@ -0,0 +1,15 @@ +package org.codewithoutus.tgbotusers.model.repository; + +import org.codewithoutus.tgbotusers.model.entity.UserJoiningNotification; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface UserJoiningNotificationRepository extends CrudRepository { + + List findByUserJoining_ChatIdAndUserJoining_AnniversaryNumberAndHasKeyboard(Long chatId, Integer anniversaryNumber, boolean hasKeyboard); + + List findByUserJoining_ChatIdAndUserJoining_UserIdAndHasKeyboard(Long chatId, Long userId, boolean hasKeyboard); +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/model/repository/UserJoiningRepository.java b/src/main/java/org/codewithoutus/tgbotusers/model/repository/UserJoiningRepository.java new file mode 100644 index 0000000..b51c4e6 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/model/repository/UserJoiningRepository.java @@ -0,0 +1,29 @@ +package org.codewithoutus.tgbotusers.model.repository; + +import org.codewithoutus.tgbotusers.model.entity.UserJoining; +import org.codewithoutus.tgbotusers.model.enums.CongratulateStatus; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; + +@Repository +public interface UserJoiningRepository extends CrudRepository { + + List findDistinctByChatIdInOrderByChatIdAscNumberAsc(Collection chatIds); + + boolean existsByChatIdAndAnniversaryNumberAndStatus(Long chatId, Integer anniversaryNumber, CongratulateStatus congratulateStatus); + + boolean existsByChatIdAndUserId(Long chatId, Long userId); + + @Query(""" + select u from UserJoining u + where (chatId, anniversaryNumber) not in ( + select distinct u.chatId, u.anniversaryNumber from UserJoining u + where u.status = ?2) + and u.chatId in ?1 + order by u.chatId, u.number, u.joinTime""") + List findByChatIdAndNotStatus(List chatIds, CongratulateStatus status); +} diff --git a/src/main/java/org/codewithoutus/tgbotusers/model/service/ChatModeratorService.java b/src/main/java/org/codewithoutus/tgbotusers/model/service/ChatModeratorService.java new file mode 100644 index 0000000..878a9fe --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/model/service/ChatModeratorService.java @@ -0,0 +1,47 @@ +package org.codewithoutus.tgbotusers.model.service; + +import lombok.RequiredArgsConstructor; +import org.codewithoutus.tgbotusers.model.entity.ChatModerator; +import org.codewithoutus.tgbotusers.model.repository.ChatModeratorRepository; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class ChatModeratorService { + + private final ChatModeratorRepository chatModeratorRepository; + + public void deleteAll() { + chatModeratorRepository.deleteAll(); + } + + public void deleteById(Integer id) { + chatModeratorRepository.deleteById(id); + } + + public ChatModerator save(ChatModerator entity) { + return chatModeratorRepository.save(entity); + } + + public List findAll() { + List result = new ArrayList<>(); + chatModeratorRepository.findAll().forEach(result::add); + return result; + } + + public Optional findByChatId(Long chatId) { + return chatModeratorRepository.findByChatId(chatId); + } + + public List findByChatUsersId(Long chatId) { + return chatModeratorRepository.findByChatUsers_ChatId(chatId); + } + + public boolean existsByChatId(Long chatId) { + return chatModeratorRepository.existsByChatId(chatId); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/model/service/ChatUserService.java b/src/main/java/org/codewithoutus/tgbotusers/model/service/ChatUserService.java new file mode 100644 index 0000000..561124c --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/model/service/ChatUserService.java @@ -0,0 +1,44 @@ +package org.codewithoutus.tgbotusers.model.service; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.codewithoutus.tgbotusers.model.entity.ChatUser; +import org.codewithoutus.tgbotusers.model.repository.ChatUserRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Getter +@Setter +@Service +@RequiredArgsConstructor +public class ChatUserService { + + private final ChatUserRepository chatUserRepository; + + public void deleteAll() { + chatUserRepository.deleteAll(); + } + + public void deleteById(Integer id) { + chatUserRepository.deleteById(id); + } + + public ChatUser save(ChatUser entity) { + return chatUserRepository.save(entity); + } + + public List findAll() { + return (List) chatUserRepository.findAll(); + } + + public Optional findByChatId(long chatId) { + return chatUserRepository.findByChatId(chatId); + } + + public boolean existByChatId(long chatId) { + return chatUserRepository.existsByChatId(chatId); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/model/service/UserJoiningNotificationService.java b/src/main/java/org/codewithoutus/tgbotusers/model/service/UserJoiningNotificationService.java new file mode 100644 index 0000000..f4c79d8 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/model/service/UserJoiningNotificationService.java @@ -0,0 +1,27 @@ +package org.codewithoutus.tgbotusers.model.service; + +import lombok.RequiredArgsConstructor; +import org.codewithoutus.tgbotusers.model.entity.UserJoiningNotification; +import org.codewithoutus.tgbotusers.model.repository.UserJoiningNotificationRepository; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UserJoiningNotificationService { + + private final UserJoiningNotificationRepository userJoiningNotificationRepository; + + public UserJoiningNotification save(UserJoiningNotification entity) { + return userJoiningNotificationRepository.save(entity); + } + + public List findByChatIdAndAnniversaryNumberAndKeyboardStatus(Long chatId, Integer anniversaryNumber, boolean hasKeyboard) { + return userJoiningNotificationRepository.findByUserJoining_ChatIdAndUserJoining_AnniversaryNumberAndHasKeyboard(chatId, anniversaryNumber, hasKeyboard); + } + + public List findByChatIdAndUserIdAndKeyboardStatus(Long chatId, Long userId, boolean hasKeyboard) { + return userJoiningNotificationRepository.findByUserJoining_ChatIdAndUserJoining_UserIdAndHasKeyboard(chatId, userId, hasKeyboard); + } +} \ No newline at end of file diff --git a/src/main/java/org/codewithoutus/tgbotusers/model/service/UserJoiningService.java b/src/main/java/org/codewithoutus/tgbotusers/model/service/UserJoiningService.java new file mode 100644 index 0000000..a5d6894 --- /dev/null +++ b/src/main/java/org/codewithoutus/tgbotusers/model/service/UserJoiningService.java @@ -0,0 +1,41 @@ +package org.codewithoutus.tgbotusers.model.service; + +import lombok.RequiredArgsConstructor; +import org.codewithoutus.tgbotusers.model.entity.UserJoining; +import org.codewithoutus.tgbotusers.model.enums.CongratulateStatus; +import org.codewithoutus.tgbotusers.model.repository.UserJoiningRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class UserJoiningService { + + private final UserJoiningRepository userJoiningRepository; + + public UserJoining save(UserJoining userJoining) { + return userJoiningRepository.save(userJoining); + } + + public Optional findById(int id) { + return userJoiningRepository.findById(id); + } + + public List findByChatIds(List chatIds) { + return userJoiningRepository.findDistinctByChatIdInOrderByChatIdAscNumberAsc(chatIds); + } + + public List findNotCongratulatedByChatIds(List chatIds) { + return userJoiningRepository.findByChatIdAndNotStatus(chatIds, CongratulateStatus.CONGRATULATE); + } + + public boolean existCongratulatedUser(long chatId, int anniversaryNumber) { + return userJoiningRepository.existsByChatIdAndAnniversaryNumberAndStatus(chatId, anniversaryNumber, CongratulateStatus.CONGRATULATE); + } + + public boolean userWasAlreadyJoinedToChat(Long chatId, Long userId) { + return userJoiningRepository.existsByChatIdAndUserId(chatId, userId); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..11e326a --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,30 @@ +bot-settings: + bot-user-name: ${BOT_USER_NAME} + bot-token: ${BOT_TOKEN} + long-polling-timeout: 75000 + +spring: + datasource: + driver-class-name: org.postgresql.Driver + url: ${DATASOURCE_URL} + username: ${DATASOURCE_USER} + password: ${DATASOURCE_PASS} + hikari: + maximum-pool-size: 2 + + jpa: + database-platform: org.hibernate.dialect.PostgreSQLDialect + show_sql: true + hibernate: + ddl-auto: update + + profiles: + include: + - bot + +logging: + level: + root: off + org.codewithoutus.tgbotusers: debug + web: off + sql: off diff --git a/src/test/java/org/codewithoutus/tgbotusers/TgBotUsersApplicationTests.java b/src/test/java/org/codewithoutus/tgbotusers/TgBotUsersApplicationTests.java new file mode 100644 index 0000000..fa2407b --- /dev/null +++ b/src/test/java/org/codewithoutus/tgbotusers/TgBotUsersApplicationTests.java @@ -0,0 +1,15 @@ +package org.codewithoutus.tgbotusers; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles({"test", "bot"}) +class TgBotUsersApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/org/codewithoutus/tgbotusers/bot/handler/AdminMessageHandlerTest.java b/src/test/java/org/codewithoutus/tgbotusers/bot/handler/AdminMessageHandlerTest.java new file mode 100644 index 0000000..6b7e9b2 --- /dev/null +++ b/src/test/java/org/codewithoutus/tgbotusers/bot/handler/AdminMessageHandlerTest.java @@ -0,0 +1,81 @@ +package org.codewithoutus.tgbotusers.bot.handler; + +import com.pengrad.telegrambot.request.SendMessage; +import org.codewithoutus.tgbotusers.bot.enums.BotCommand; +import org.codewithoutus.tgbotusers.bot.service.TelegramService; +import org.codewithoutus.tgbotusers.mocks.answer.AnswerChatModerator; +import org.codewithoutus.tgbotusers.mocks.answer.AnswerSendResponse; +import org.codewithoutus.tgbotusers.mocks.dto.Chat; +import org.codewithoutus.tgbotusers.mocks.dto.Message; +import org.codewithoutus.tgbotusers.mocks.dto.Update; +import org.codewithoutus.tgbotusers.model.entity.ChatModerator; +import org.codewithoutus.tgbotusers.model.service.ChatModeratorService; +import org.codewithoutus.tgbotusers.model.service.ChatUserService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class AdminMessageHandlerTest { + @Mock + private TelegramService telegramService; + @Mock + private ChatModeratorService chatModeratorService; + @Mock + private ChatUserService chatUserService; + + @InjectMocks + private AdminMessageHandler adminMessageHandler; + + @DisplayName("Тест добавления чата модераторов: добавление уже имеющегося в базе ID") + @Test + public void testAddModerChat_existId() { + // when + ChatModerator entity = new ChatModerator(); + when(chatModeratorService.findByChatId(any(Long.class))).thenReturn(Optional.of(entity)); + when(telegramService.sendMessage(any(SendMessage.class))).then(AnswerSendResponse.sendToConsole()); + Mockito.lenient().when(chatModeratorService.save(any(ChatModerator.class))).thenThrow(new IllegalStateException("Записи в БД не должно быть")); + + // then + String commandText = BotCommand.ADD_MODER_CHAT.getText() + " 123"; // "/addModerChat 123" + com.pengrad.telegrambot.model.Update update = getPrivateMessageUpdateWithText(commandText); + + assertTrue(adminMessageHandler.handle(update)); + assertNull(entity.getId()); + } + + @DisplayName("Тест добавления чата модераторов: добавление нового ID") + @Test + public void testAddModerChat_newId() { + // when + ChatModerator entity = new ChatModerator(); + when(chatModeratorService.findByChatId(any(Long.class))).thenReturn(Optional.empty()); + when(chatModeratorService.save(any(ChatModerator.class))).then(AnswerChatModerator.save(entity)); + + // when + String commandText = BotCommand.ADD_MODER_CHAT.getText() + " 123"; // "/addModerChat 123" + com.pengrad.telegrambot.model.Update update = getPrivateMessageUpdateWithText(commandText); + + assertTrue(adminMessageHandler.handle(update)); + assertNotNull(entity.getId()); + } + + private Update getPrivateMessageUpdateWithText(String text) { + return Update.builder() + .message(Message.builder() + .text(text) + .chat(Chat.builder().type(com.pengrad.telegrambot.model.Chat.Type.Private).build()) + .build()) + .build(); + } +} diff --git a/src/test/java/org/codewithoutus/tgbotusers/mocks/answer/AnswerChatModerator.java b/src/test/java/org/codewithoutus/tgbotusers/mocks/answer/AnswerChatModerator.java new file mode 100644 index 0000000..61165d8 --- /dev/null +++ b/src/test/java/org/codewithoutus/tgbotusers/mocks/answer/AnswerChatModerator.java @@ -0,0 +1,33 @@ +package org.codewithoutus.tgbotusers.mocks.answer; + +import org.codewithoutus.tgbotusers.model.entity.ChatModerator; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.concurrent.atomic.AtomicInteger; + +public class AnswerChatModerator { + + private static final AtomicInteger sequence = new AtomicInteger(); + + public static Answer newEntity() { + return new Answer() { + @Override + public ChatModerator answer(InvocationOnMock invocation) throws Throwable { + ChatModerator entity = new ChatModerator(); + entity.setId(sequence.getAndIncrement()); + return entity; + } + }; + } + + public static Answer save(ChatModerator entity) { + return new Answer() { + @Override + public ChatModerator answer(InvocationOnMock invocation) throws Throwable { + entity.setId(sequence.getAndIncrement()); + return entity; + } + }; + } +} diff --git a/src/test/java/org/codewithoutus/tgbotusers/mocks/answer/AnswerSendResponse.java b/src/test/java/org/codewithoutus/tgbotusers/mocks/answer/AnswerSendResponse.java new file mode 100644 index 0000000..1ff915d --- /dev/null +++ b/src/test/java/org/codewithoutus/tgbotusers/mocks/answer/AnswerSendResponse.java @@ -0,0 +1,21 @@ +package org.codewithoutus.tgbotusers.mocks.answer; + +import com.pengrad.telegrambot.request.SendMessage; +import com.pengrad.telegrambot.response.SendResponse; +import org.codewithoutus.tgbotusers.mocks.response.ResponseUtils; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +public class AnswerSendResponse { + + public static Answer sendToConsole() { + return new Answer() { + @Override + public SendResponse answer(InvocationOnMock invocationOnMock) throws Throwable { + SendMessage sendMessage = (SendMessage) invocationOnMock.getArgument(0); + System.out.println(sendMessage.getParameters().get("text")); + return null; + } + }; + } +} diff --git a/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/CallbackQuery.java b/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/CallbackQuery.java new file mode 100644 index 0000000..4849b4c --- /dev/null +++ b/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/CallbackQuery.java @@ -0,0 +1,8 @@ +package org.codewithoutus.tgbotusers.mocks.dto; + +import lombok.Builder; + +@Builder +public class CallbackQuery extends com.pengrad.telegrambot.model.CallbackQuery { + private String data; +} diff --git a/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/Chat.java b/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/Chat.java new file mode 100644 index 0000000..c84dff4 --- /dev/null +++ b/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/Chat.java @@ -0,0 +1,25 @@ +package org.codewithoutus.tgbotusers.mocks.dto; + +import lombok.Builder; + +@Builder +public class Chat extends com.pengrad.telegrambot.model.Chat { + private Long id; + private Type type; + private String title; + + @Override + public Long id() { + return id; + } + + @Override + public Type type() { + return type; + } + + @Override + public String title() { + return title; + } +} \ No newline at end of file diff --git a/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/ChatJoinRequest.java b/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/ChatJoinRequest.java new file mode 100644 index 0000000..ab113bf --- /dev/null +++ b/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/ChatJoinRequest.java @@ -0,0 +1,27 @@ +package org.codewithoutus.tgbotusers.mocks.dto; + +import com.pengrad.telegrambot.model.Chat; +import com.pengrad.telegrambot.model.User; +import lombok.Builder; + +@Builder +public class ChatJoinRequest extends com.pengrad.telegrambot.model.ChatJoinRequest { + private Chat chat; + private User from; + private Integer date; + + @Override + public Chat chat() { + return chat; + } + + @Override + public User from() { + return from; + } + + @Override + public Integer date() { + return date; + } +} diff --git a/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/Message.java b/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/Message.java new file mode 100644 index 0000000..13ef193 --- /dev/null +++ b/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/Message.java @@ -0,0 +1,50 @@ +package org.codewithoutus.tgbotusers.mocks.dto; + +import lombok.Builder; +import lombok.Getter; + +@Builder +public class Message extends com.pengrad.telegrambot.model.Message { + private Integer message_id; + private User from; + private Integer date; + private Chat chat; + private Integer forward_date; + private String text; + private MessageEntity[] entities; + + @Override + public Integer messageId() { + return message_id; + } + + @Override + public com.pengrad.telegrambot.model.User from() { + return from; + } + + @Override + public Integer date() { + return date; + } + + @Override + public com.pengrad.telegrambot.model.Chat chat() { + return chat; + } + + @Override + public Integer forwardDate() { + return forward_date; + } + + @Override + public String text() { + return text; + } + + @Override + public com.pengrad.telegrambot.model.MessageEntity[] entities() { + return entities; + } +} diff --git a/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/MessageEntity.java b/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/MessageEntity.java new file mode 100644 index 0000000..f3813a7 --- /dev/null +++ b/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/MessageEntity.java @@ -0,0 +1,29 @@ +package org.codewithoutus.tgbotusers.mocks.dto; + +import lombok.Builder; + +public class MessageEntity extends com.pengrad.telegrambot.model.MessageEntity { + private Type type; + private Integer offset; + private Integer length; + + @Builder + public MessageEntity(Type type, Integer offset, Integer length) { + super(type, offset, length); + } + + @Override + public Type type() { + return type; + } + + @Override + public Integer offset() { + return offset; + } + + @Override + public Integer length() { + return length; + } +} diff --git a/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/Update.java b/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/Update.java new file mode 100644 index 0000000..b997d22 --- /dev/null +++ b/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/Update.java @@ -0,0 +1,25 @@ +package org.codewithoutus.tgbotusers.mocks.dto; + +import lombok.Builder; + +@Builder +public class Update extends com.pengrad.telegrambot.model.Update { + private Message message; + private CallbackQuery callback_query; + private ChatJoinRequest chat_join_request; + + @Override + public com.pengrad.telegrambot.model.Message message() { + return message; + } + + @Override + public com.pengrad.telegrambot.model.CallbackQuery callbackQuery() { + return callback_query; + } + + @Override + public com.pengrad.telegrambot.model.ChatJoinRequest chatJoinRequest() { + return chat_join_request; + } +} diff --git a/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/User.java b/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/User.java new file mode 100644 index 0000000..ed1a2f4 --- /dev/null +++ b/src/test/java/org/codewithoutus/tgbotusers/mocks/dto/User.java @@ -0,0 +1,41 @@ +package org.codewithoutus.tgbotusers.mocks.dto; + +import lombok.Builder; + +public class User extends com.pengrad.telegrambot.model.User { + private Long id; + private Boolean is_bot; + private String first_name; + private String last_name; + private String username; + + @Builder + public User(Long id) { + super(id); + } + + @Override + public Long id() { + return id; + } + + @Override + public Boolean isBot() { + return is_bot; + } + + @Override + public String firstName() { + return first_name; + } + + @Override + public String lastName() { + return last_name; + } + + @Override + public String username() { + return username; + } +} diff --git a/src/test/java/org/codewithoutus/tgbotusers/mocks/response/ResponseUtils.java b/src/test/java/org/codewithoutus/tgbotusers/mocks/response/ResponseUtils.java new file mode 100644 index 0000000..e49db8e --- /dev/null +++ b/src/test/java/org/codewithoutus/tgbotusers/mocks/response/ResponseUtils.java @@ -0,0 +1,12 @@ +package org.codewithoutus.tgbotusers.mocks.response; + +import com.pengrad.telegrambot.response.SendResponse; +import lombok.SneakyThrows; + +public class ResponseUtils { + + @SneakyThrows + public static SendResponse newSendResponse() { + return SendResponse.class.getConstructor().newInstance(); + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..06c1456 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,30 @@ +#server: +# port: 8090 + +bot-settings: +# bot-user-name: GroupControlSkillboxBot +# bot-token: 5566628073:AAHyZTYOh62Fb7_zVGNmHMqzwJUQtBvGWuc + long-polling-timeout: 75000 + +spring: + datasource: + url: jdbc:postgresql://localhost:5432/tg-bot-users + username: tg-admin + password: 9en2w0oc +# driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: 2 + + jpa: +# database-platform: org.hibernate.dialect.PostgreSQLDialect + show_sql: false + hibernate: + ddl-auto: update + + +logging: + level: + root: info + org.codewithoutus.tgbotusers: debug + web: off + sql: off