From a608baeed738894433aacfa041e2617f60ce959f Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Sat, 4 Apr 2020 01:15:01 +0300 Subject: Initialize all components from configuration --- src/main/java/com/juick/TelegramBotManager.java | 465 ++++++++++++++++++++++++ 1 file changed, 465 insertions(+) create mode 100644 src/main/java/com/juick/TelegramBotManager.java (limited to 'src/main/java/com/juick/TelegramBotManager.java') diff --git a/src/main/java/com/juick/TelegramBotManager.java b/src/main/java/com/juick/TelegramBotManager.java new file mode 100644 index 00000000..3538a27b --- /dev/null +++ b/src/main/java/com/juick/TelegramBotManager.java @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2008-2020, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick; + +import com.juick.model.User; +import com.juick.model.AnonymousUser; +import com.juick.model.CommandResult; +import com.juick.www.api.SystemActivity; +import com.juick.util.HttpUtils; +import com.juick.service.MessagesService; +import com.juick.service.TelegramService; +import com.juick.service.UserService; +import com.juick.service.component.SystemEvent; +import com.juick.service.component.NotificationListener; +import com.juick.service.component.PingEvent; +import com.juick.util.MessageUtils; +import com.pengrad.telegrambot.Callback; +import com.pengrad.telegrambot.TelegramBot; +import com.pengrad.telegrambot.UpdatesListener; +import com.pengrad.telegrambot.model.Message; +import com.pengrad.telegrambot.model.MessageEntity; +import com.pengrad.telegrambot.model.PhotoSize; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.ParseMode; +import com.pengrad.telegrambot.request.GetFile; +import com.pengrad.telegrambot.request.SendMessage; +import com.pengrad.telegrambot.request.SendPhoto; +import com.pengrad.telegrambot.request.SetWebhook; +import com.pengrad.telegrambot.response.GetFileResponse; +import com.pengrad.telegrambot.response.SendResponse; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.*; + +import static com.juick.util.formatters.PlainTextFormatter.formatPost; +import static com.juick.util.formatters.PlainTextFormatter.formatUrl; + +/** + * Created by vt on 12/05/16. + */ +public class TelegramBotManager implements NotificationListener { + private static final Logger logger = LoggerFactory.getLogger("Telegram"); + + private TelegramBot bot; + + @Value("${telegram_api_url:}") + private String apiUrl; + @Value("${telegram_file_api_url:}") + private String fileApiUrl; + @Value("${telegram_webhook_url:}") + private String webhookUrl; + @Value("${telegram_token:12345678}") + private String telegramToken; + @Value("${telegram_debug:false}") + private boolean telegramDebug; + @Inject + private TelegramService telegramService; + @Inject + private MessagesService messagesService; + @Inject + private UserService userService; + @Inject + private CommandsManager commandsManager; + @Inject + private ApplicationEventPublisher applicationEventPublisher; + @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String tmpDir; + @Value("${service_user:juick}") + private String serviceUser; + + private static final String MSG_LINK = "🔗"; + + @PostConstruct + public void init() { + TelegramBot.Builder tgBuilder = new TelegramBot.Builder(telegramToken); + if (StringUtils.isNotEmpty(apiUrl)) { + tgBuilder.apiUrl(apiUrl).fileApiUrl(fileApiUrl); + } + bot = tgBuilder.build(); + if (!telegramDebug) { + try { + SetWebhook webhook = new SetWebhook().url(webhookUrl); + if (!bot.execute(webhook).isOk()) { + logger.error("error setting webhook"); + } + } catch (Exception e) { + logger.warn("couldn't initialize telegram bot", e); + } + } else { + bot.setUpdatesListener(updates -> { + logger.info("got updates: {}", updates); + updates.forEach(this::processUpdate); + return UpdatesListener.CONFIRMED_UPDATES_ALL; + }); + } + } + + public void processUpdate(Update update) { + Message message = update.message(); + if (update.message() == null) { + message = update.editedMessage(); + if (message == null) { + logger.error("error parsing telegram update: {}", update); + return; + } + User user_from = userService.getUserByUID(telegramService.getUser(message.chat().id())).orElse(AnonymousUser.INSTANCE); + logger.info("Found juick user {}", user_from.getUid()); + Optional> originalMessageData = messagesService.findMessageByProperty("durovId", + String.valueOf(message.messageId())); + if (originalMessageData.isPresent()) { + int mid = originalMessageData.get().getLeft(); + int rid = originalMessageData.get().getRight(); + // TODO: this is copypaste from api, need switch to api + com.juick.model.Message originalMessage = rid == 0 ? messagesService.getMessage(mid).orElseThrow(IllegalStateException::new) + : messagesService.getReply(mid, rid); + User author = originalMessage.getUser(); + String newMessageText = StringUtils.defaultString(message.text()); + if (user_from.equals(author) && canUpdateMessage(originalMessage, newMessageText)) { + if (messagesService.updateMessage(mid, rid, newMessageText)) { + telegramNotify(message.chat().id(), "Message updated", new com.juick.model.Message()); + return; + } + } + telegramNotify(message.chat().id(), "Error updating message", new com.juick.model.Message()); + } + } else { + User user_from = userService.getUserByUID(telegramService.getUser(message.chat().id())).orElse(AnonymousUser.INSTANCE); + logger.info("Found juick user {}", user_from.getUid()); + + String username = message.from().username(); + if (username == null) { + username = message.from().firstName(); + } + if (!user_from.isAnonymous()) { + URI attachment = URI.create(StringUtils.EMPTY); + if (message.photo() != null) { + String fileId = Arrays.stream(message.photo()).max(Comparator.comparingInt(PhotoSize::fileSize)) + .orElse(new PhotoSize()).fileId(); + if (StringUtils.isNotEmpty(fileId)) { + GetFile request = new GetFile(fileId); + GetFileResponse response = bot.execute(request); + logger.info("got file {}", response.file()); + try { + URL fileURL = new URL(bot.getFullFilePath(response.file())); + attachment = HttpUtils.downloadImage(fileURL, tmpDir); + } catch (Exception e) { + logger.warn("attachment exception", e); + } + logger.info("received {}", attachment); + } + } + String text = message.text(); + if (StringUtils.isBlank(text)) { + text = message.caption(); + } + if (StringUtils.isBlank(text)) { + text = StringUtils.EMPTY; + } + if (StringUtils.isNotEmpty(text) || StringUtils.isNotEmpty(attachment.toString())) { + if (text.equalsIgnoreCase("LOGIN") + || text.equalsIgnoreCase("PING") + || text.equalsIgnoreCase("HELP") + || text.equalsIgnoreCase("/login") + || text.equalsIgnoreCase("/logout") + || text.equalsIgnoreCase("/start") + || text.equalsIgnoreCase("/help")) { + String msgUrl = "http://juick.com/login?hash=" + userService.getHashByUID(user_from.getUid()); + String msg = String.format("Hi, %s!\nYou can post messages and images to Juick there.\n" + + "Tap to [log into website](%s) to get more info", user_from.getName(), msgUrl); + telegramNotify(message.from().id().longValue(), msg, new com.juick.model.Message()); + } else { + Message replyMessage = message.replyToMessage(); + if (replyMessage != null) { + MessageEntity[] entities = replyMessage.entities(); + if (entities == null) { + entities = replyMessage.captionEntities(); + } + if (entities != null) { + Optional juickLink = Arrays.stream(entities) + .filter(this::isJuickLink) + .findFirst(); + if (juickLink.isPresent()) { + if (StringUtils.isNotEmpty(juickLink.get().url())) { + UriComponents uriComponents = UriComponentsBuilder.fromUriString( + juickLink.get().url()).build(); + String path = uriComponents.getPath(); + if (StringUtils.isNotEmpty(path) && path.length() > 1) { + int mid; + try { + mid = Integer.parseInt(path.substring(3)); + } catch (NumberFormatException e) { + logger.warn("wrong mid received"); + return; + } + String prefix = String.format("#%d ", mid); + if (StringUtils.isNotEmpty(uriComponents.getFragment())) { + int rid = Integer.parseInt(uriComponents.getFragment()); + prefix = String.format("#%d/%d ", mid, rid); + } + executeCommand(message.messageId(), message.from().id().longValue(), + user_from, prefix + text, attachment); + } else { + logger.warn("invalid path: {}", path); + } + } else { + logger.warn("invalid entity: {}", juickLink); + } + } else { + telegramNotify(message.from().id().longValue(), + "Can not reply to this message", replyMessage.messageId(), new com.juick.model.Message()); + } + } else { + telegramNotify(message.from().id().longValue(), + "Can not reply to this message", replyMessage.messageId(), new com.juick.model.Message()); + } + } else { + executeCommand(message.messageId(), message.from().id().longValue(), + user_from, text, attachment); + } + } + } + messagesService.getUnread(user_from).forEach(mid -> messagesService.setRead(user_from, mid)); + } else { + List chats = telegramService.getAnonymous(); + if (!chats.contains(message.chat().id())) { + logger.info("added chat with {}", username); + telegramService.createTelegramUser(message.from().id(), username); + } + telegramSignupNotify(message.from().id().longValue(), userService.getSignUpHashByTelegramID(message.from().id().longValue(), username)); + } + } + } + + /* + validate user input + */ + private boolean canUpdateMessage(com.juick.model.Message message, String newData) { + if (StringUtils.isEmpty(newData)) { + // allow empty text only when image is present + return StringUtils.isNotEmpty(message.getAttachmentType()); + } + return true; + } + + private void executeCommand(Integer messageId, Long userId, User user_from, String text, URI attachment) { + try { + CommandResult result = commandsManager.processCommand(user_from, text, attachment); + if (result.getNewMessage().isPresent()) { + com.juick.model.Message newMessage = result.getNewMessage().get(); + messagesService.setMessageProperty(newMessage.getMid(), newMessage.getRid(), "durovId", + String.valueOf(messageId)); + } + String messageTxt = StringUtils.isNotEmpty(result.getMarkdown()) ? result.getMarkdown() + : "Unknown error or unsupported command"; + telegramNotify(userId, messageTxt, new com.juick.model.Message()); + } catch (Exception e) { + logger.warn("telegram exception", e); + } + } + + private boolean isJuickLink(MessageEntity e) { + return e.offset() == 0 && e.type().equals(MessageEntity.Type.text_link) && e.length() == 2; + } + + public void telegramNotify(Long chatId, String msg, @Nonnull com.juick.model.Message source) { + telegramNotify(chatId, msg, 0, source); + } + + public void telegramNotify(Long chatId, String msg, Integer replyTo, @Nonnull com.juick.model.Message source) { + String attachment = MessageUtils.attachmentUrl(source); + boolean isSendTxt = true; + if (!StringUtils.isEmpty(attachment)) { + SendPhoto telegramPhoto = new SendPhoto(chatId, attachment); + if (replyTo > 0) { + telegramPhoto.replyToMessageId(replyTo); + } + telegramPhoto.parseMode(ParseMode.Markdown); + if(msg.length() < 1024) { + telegramPhoto.caption(msg); + isSendTxt = false; + } + bot.execute(telegramPhoto, new Callback<>() { + @Override + public void onResponse(SendPhoto request, SendResponse response) { + processTelegramResponse(chatId, response, source); + } + + @Override + public void onFailure(SendPhoto request, IOException e) { + logger.warn("telegram failure", e); + } + }); + } + if (isSendTxt){ + SendMessage telegramMessage = new SendMessage(chatId, msg); + if (replyTo > 0) { + telegramMessage.replyToMessageId(replyTo); + } + telegramMessage.parseMode(ParseMode.Markdown).disableWebPagePreview(true); + bot.execute(telegramMessage, new Callback<>() { + @Override + public void onResponse(SendMessage request, SendResponse response) { + processTelegramResponse(chatId, response, source); + } + + @Override + public void onFailure(SendMessage request, IOException e) { + logger.warn("telegram failure", e); + } + }); + } + } + + private void processTelegramResponse(Long chatId, SendResponse response, com.juick.model.Message source) { + int userId = telegramService.getUser(chatId); + if (!response.isOk()) { + if (response.errorCode() == 403) { + // remove from anonymous users + telegramService.getAnonymous().stream().filter(c -> c.equals(chatId)).findFirst().ifPresent( + d -> { + telegramService.deleteAnonymous(d); + logger.info("deleted {} chat", d); + } + ); + if (userId > 0) { + User userToDelete = userService.getUserByUID(userId) + .orElseThrow(IllegalStateException::new); + boolean status = telegramService.deleteTelegramUser(userToDelete.getUid()); + logger.info("deleting telegram id of @{} : {}", userToDelete.getName(), status); + } + } else { + logger.warn("error response, isOk: {}, errorCode: {}, description: {}", + response.isOk(), response.errorCode(), response.description()); + } + } else { + if (MessageUtils.isReply(source)) { + messagesService.setLastReadComment(userService.getUserByUID(userId) + .orElseThrow(IllegalStateException::new), source.getMid(), source.getRid()); + User user = userService.getUserByUID(userId).orElseThrow(IllegalStateException::new); + userService.updateLastSeen(user); + applicationEventPublisher.publishEvent( + new SystemEvent(this, SystemActivity.read(user, source))); + } + } + } + + public void telegramSignupNotify(Long telegramId, String hash) { + bot.execute(new SendMessage(telegramId, + String.format("You are subscribed to all Juick messages. " + + "[Create or link](http://juick.com/signup?type=durov&hash=%s) " + + "an existing Juick account to get your subscriptions and ability to post messages", hash)) + .parseMode(ParseMode.Markdown), new Callback<>() { + @Override + public void onResponse(SendMessage request, SendResponse response) { + logger.info("got response: {}", response.message()); + } + + @Override + public void onFailure(SendMessage request, IOException e) { + logger.warn("telegram failure", e); + } + }); + } + + @Override + public void processSystemEvent(SystemEvent systemEvent) { + var activity = systemEvent.getActivity(); + var type = activity.getType(); + if (type.equals(SystemActivity.ActivityType.message)) { + processMessage(activity.getMessage(), activity.getTo()); + } else if (type.equals(SystemActivity.ActivityType.like)) { + if (systemEvent.getActivity().getFrom().getName().equals(serviceUser)) { + processTop(systemEvent.getActivity().getMessage()); + } else { + processLike(activity.getFrom(), activity.getMessage(), activity.getTo()); + } + } else if (type.equals(SystemActivity.ActivityType.follow)) { + processFollow(activity.getFrom(), activity.getTo()); + } + } + private void processMessage(com.juick.model.Message jmsg, List subscribedUsers) { + if (jmsg.isService()) { + return; + } + String msgUrl = formatUrl(jmsg); + if (MessageUtils.isPM(jmsg)) { + telegramService.getTelegramIdentifiers(Collections.singletonList(jmsg.getTo())) + .forEach(c -> telegramNotify(c, formatPost(jmsg, true), jmsg)); + } else if (MessageUtils.isReply(jmsg)) { + String fmsg = String.format("[%s](%s) %s", MSG_LINK, msgUrl, formatPost(jmsg, true)); + telegramService.getTelegramIdentifiers( + subscribedUsers + ).forEach(c -> telegramNotify(c, fmsg, jmsg)); + } else { + String msg = String.format("[%s](%s) %s", MSG_LINK, msgUrl, formatPost(jmsg, true)); + + List users = telegramService.getTelegramIdentifiers(subscribedUsers); + List chats = telegramService.getAnonymous(); + // registered subscribed users + + users.forEach(c -> telegramNotify(c, msg, jmsg)); + // anonymous + chats.stream().filter(u -> telegramService.getUser(u) == 0).forEach(c -> telegramNotify(c, msg, jmsg)); + } + } + + private void processLike(User liker, com.juick.model.Message message, List subscribers) { + if (!liker.getName().equals(serviceUser)) { + logger.info("Like received in tg listener"); + if (!userService.isInBLAny(message.getUser().getUid(), liker.getUid())) { + telegramService.getTelegramIdentifiers(Collections.singletonList(message.getUser())) + .forEach(c -> telegramNotify(c, String.format("%s recommends your [post](%s)", + MessageUtils.getMarkdownUser(liker), formatUrl(message)), new com.juick.model.Message())); + } + telegramService.getTelegramIdentifiers(subscribers) + .forEach(c -> telegramNotify(c, String.format("%s recommends you someone's [post](%s)", + MessageUtils.getMarkdownUser(liker), formatUrl(message)), new com.juick.model.Message())); + } + } + + @Override + public void processPingEvent(PingEvent pingEvent) { + + } + + private void processTop(com.juick.model.Message message) { + telegramService.getTelegramIdentifiers(Collections.singletonList(message.getUser())) + .forEach(c -> telegramNotify(c, String.format("Your [post](%s) became popular!", + formatUrl(message)), new com.juick.model.Message())); + } + + private void processFollow(User subscriber, List target) { + telegramService.getTelegramIdentifiers(target) + .forEach(c -> telegramNotify(c, String.format("%s subscribed to your blog", + MessageUtils.getMarkdownUser(subscriber)), new com.juick.model.Message())); + } +} -- cgit v1.2.3