package com.notononoto.teamcity.telegram; import com.intellij.openapi.diagnostic.Logger; import com.notononoto.teamcity.telegram.config.TelegramSettingsManager; import freemarker.core.Environment; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; import freemarker.template.TemplateExceptionHandler; import jetbrains.buildServer.Build; import jetbrains.buildServer.log.Loggers; import jetbrains.buildServer.notification.NotificatorAdapter; import jetbrains.buildServer.notification.NotificatorRegistry; import jetbrains.buildServer.notification.TemplateMessageBuilder; import jetbrains.buildServer.responsibility.ResponsibilityEntry; import jetbrains.buildServer.responsibility.TestNameResponsibilityEntry; import jetbrains.buildServer.serverSide.*; import jetbrains.buildServer.serverSide.mute.MuteInfo; import jetbrains.buildServer.serverSide.problems.BuildProblemInfo; import jetbrains.buildServer.tests.TestName; import jetbrains.buildServer.users.NotificatorPropertyKey; import jetbrains.buildServer.users.PropertyKey; import jetbrains.buildServer.users.SUser; import jetbrains.buildServer.util.StringUtil; import jetbrains.buildServer.vcs.VcsRoot; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.StringWriter; import java.nio.file.Path; import java.util.*; import java.util.stream.Collectors; /** * Telegram notifier */ public class TelegramNotificator extends NotificatorAdapter { /** * In {@link Loggers} doesn't exists NOTIFIER entry. Package renaming to * jetbrains.buildServer something very strange. But maybe it's required... * So don't use this log here too active... */ private static final Logger LOG = Loggers.SERVER; /** * Name of message variable in FreeMarker context after template execution */ private static final String FREE_MARKER_MSG_KEY = "message"; /** * Inner property name */ private static final String CHAT_ID_PROP = "telegram-chat-id"; /** * Notificator type */ private static final String NOTIFICATOR_TYPE = "telegram"; /** * Property key description */ public static final PropertyKey TELEGRAM_PROP_KEY = new NotificatorPropertyKey(NOTIFICATOR_TYPE, CHAT_ID_PROP); /** * User input field at notification rules tab */ private static List<UserPropertyInfo> USER_PROPERTIES = Collections.singletonList( new UserPropertyInfo(CHAT_ID_PROP, "Telegram chat id", null, (UserPropertyValidator) (propertyValue, editee, currentUserData) -> StringUtil.isEmpty(propertyValue) || TelegramNotificator.isLong(propertyValue) ? null : "Chat id should be a number")); /** * Telegram bot manager */ private final TelegramBotManager botManager; /** * FreeMarker message builder */ private final TemplateMessageBuilder messageBuilder; /** * Templates files config dir */ private final Configuration freeMarkerConfig; public TelegramNotificator(@NotNull NotificatorRegistry registry, @NotNull TelegramSettingsManager settingsManager, @NotNull TelegramBotManager botManager, @NotNull TemplateMessageBuilder messageBuilder) throws IOException { this.botManager = botManager; this.messageBuilder = messageBuilder; // Plugin will not work if that statement fails, so don't suppress exception here freeMarkerConfig = createFreeMarkerConfig(settingsManager.getSettingsDir()); registry.register(this, USER_PROPERTIES); } @NotNull @Override public String getNotificatorType() { return NOTIFICATOR_TYPE; } @NotNull @Override public String getDisplayName() { return "Telegram Notifier"; } @Override public void notifyBuildStarted(@NotNull SRunningBuild build, @NotNull Set<SUser> users) { Map<String, Object> props = messageBuilder.getBuildStartedMap(build, users); sendNotification(props, users, "build_started"); } @Override public void notifyBuildSuccessful(@NotNull SRunningBuild build, @NotNull Set<SUser> users) { Map<String, Object> props = messageBuilder.getBuildSuccessfulMap(build, users); sendNotification(props, users, "build_successful"); } @Override public void notifyBuildFailed(@NotNull SRunningBuild build, @NotNull Set<SUser> users) { Map<String, Object> props = messageBuilder.getBuildFailedMap(build, users); sendNotification(props, users, "build_failed"); } @Override public void notifyBuildFailedToStart(@NotNull SRunningBuild build, @NotNull Set<SUser> users) { Map<String, Object> props = messageBuilder.getBuildFailedToStartMap(build, users); sendNotification(props, users, "build_failed_to_start"); } @Override public void notifyLabelingFailed(@NotNull Build build, @NotNull VcsRoot root, @NotNull Throwable exception, @NotNull Set<SUser> users) { Map<String, Object> props = messageBuilder. getLabelingFailedMap((SBuild) build, root, exception, users); sendNotification(props, users, "labeling_failed"); } @Override public void notifyBuildFailing(@NotNull SRunningBuild build, @NotNull Set<SUser> users) { Map<String, Object> props = messageBuilder.getBuildFailedMap(build, users); sendNotification(props, users, "build_failed"); } @Override public void notifyBuildProbablyHanging(@NotNull SRunningBuild build, @NotNull Set<SUser> users) { Map<String, Object> props = messageBuilder.getBuildProbablyHangingMap(build, users); sendNotification(props, users, "build_probably_hanging"); } @Override public void notifyResponsibleChanged(@NotNull SBuildType buildType, @NotNull Set<SUser> users) { Map<String, Object> props = messageBuilder. getBuildTypeResponsibilityChangedMap(buildType, users); sendNotification(props, users, "build_type_responsibility_changed"); } @Override public void notifyResponsibleAssigned(@NotNull SBuildType buildType, @NotNull Set<SUser> users) { Map<String, Object> props = messageBuilder. getBuildTypeResponsibilityAssignedMap(buildType, users); sendNotification(props, users, "build_type_responsibility_assigned_to_me"); } @Override public void notifyResponsibleChanged(@Nullable TestNameResponsibilityEntry oldValue, @NotNull TestNameResponsibilityEntry newValue, @NotNull SProject project, @NotNull Set<SUser> users) { Map<String, Object> props = messageBuilder. getTestResponsibilityChangedMap(newValue, oldValue, project, users); sendNotification(props, users, "test_responsibility_changed"); } @Override public void notifyResponsibleAssigned(@Nullable TestNameResponsibilityEntry oldValue, @NotNull TestNameResponsibilityEntry newValue, @NotNull SProject project, @NotNull Set<SUser> users) { Map<String, Object> root = messageBuilder. getTestResponsibilityAssignedMap(newValue, oldValue, project, users); this.sendNotification(root, users, "test_responsibility_assigned_to_me"); } @Override public void notifyResponsibleChanged(@NotNull Collection<TestName> testNames, @NotNull ResponsibilityEntry entry, @NotNull SProject project, @NotNull Set<SUser> users) { Map<String, Object> root = messageBuilder. getTestResponsibilityAssignedMap(testNames, entry, project, users); this.sendNotification(root, users, "multiple_test_responsibility_changed"); } @Override public void notifyResponsibleAssigned(@NotNull Collection<TestName> testNames, @NotNull ResponsibilityEntry entry, @NotNull SProject project, @NotNull Set<SUser> users) { Map<String, Object> root = messageBuilder. getTestResponsibilityChangedMap(testNames, entry, project, users); this.sendNotification(root, users, "multiple_test_responsibility_assigned_to_me"); } @Override public void notifyBuildProblemResponsibleAssigned(@NotNull Collection<BuildProblemInfo> buildProblems, @NotNull ResponsibilityEntry entry, @NotNull SProject project, @NotNull Set<SUser> users) { Map<String, Object> root = messageBuilder. getBuildProblemsResponsibilityAssignedMap(buildProblems, entry, project, users); this.sendNotification(root, users, "build_problem_responsibility_assigned_to_me"); } @Override public void notifyBuildProblemResponsibleChanged(@NotNull Collection<BuildProblemInfo> buildProblems, @NotNull ResponsibilityEntry entry, @NotNull SProject project, @NotNull Set<SUser> users) { Map<String, Object> root = messageBuilder. getBuildProblemsResponsibilityAssignedMap(buildProblems, entry, project, users); this.sendNotification(root, users, "build_problem_responsibility_changed"); } @Override public void notifyTestsMuted(@NotNull Collection<STest> tests, @NotNull MuteInfo muteInfo, @NotNull Set<SUser> users) { Map<String, Object> root = messageBuilder. getTestsMutedMap(tests, muteInfo, users); this.sendNotification(root, users, "tests_muted"); } @Override public void notifyTestsUnmuted(@NotNull Collection<STest> tests, @NotNull MuteInfo muteInfo, @Nullable SUser user, @NotNull Set<SUser> users) { Map<String, Object> root = messageBuilder. getTestsUnmutedMap(tests, muteInfo, user, users); this.sendNotification(root, users, "tests_unmuted"); } @Override public void notifyBuildProblemsMuted(@NotNull Collection<BuildProblemInfo> buildProblems, @NotNull MuteInfo muteInfo, @NotNull Set<SUser> users) { Map<String, Object> root = messageBuilder. getBuildProblemsMutedMap(buildProblems, muteInfo, users); this.sendNotification(root, users, "build_problems_muted"); } @Override public void notifyBuildProblemsUnmuted(@NotNull Collection<BuildProblemInfo> buildProblems, @NotNull MuteInfo muteInfo, @Nullable SUser user, @NotNull Set<SUser> users) { Map<String, Object> root = messageBuilder. getBuildProblemsMutedMap(buildProblems, muteInfo, users); this.sendNotification(root, users, "build_problems_unmuted"); } /** * Send notifications to telegram users * * @param props template parameters * @param users users to send messages * @param templateName template name */ private void sendNotification(@NotNull Map<String, Object> props, @NotNull Set<SUser> users, @NotNull String templateName) { String message; try (StringWriter writer = new StringWriter()) { Template template = freeMarkerConfig.getTemplate(templateName + ".ftl"); Environment env = template.createProcessingEnvironment(props, writer, null); env.process(); if (!env.getKnownVariableNames().contains(FREE_MARKER_MSG_KEY)) { LOG.warn("Can't extract message from template. Message will not be sended"); return; } message = env.getVariable(FREE_MARKER_MSG_KEY).toString(); } catch (IOException | TemplateException ex) { LOG.error("Can't execute template '" + templateName + ".ftl': ", ex); return; } LOG.debug("Send to telegram message: " + StringUtil.truncateStringValueWithDotsAtEnd(message, 80)); collectChatIds(users).forEach(chatId -> { try { botManager.sendMessage(chatId, message); } catch (Exception ex) { LOG.warnAndDebugDetails("Can't send message to chatId='" + chatId + "'", ex); } }); } /** * @param users telegram users * @return users ids without duplicates */ private List<Long> collectChatIds(@NotNull Set<SUser> users) { return users.stream() .map(user -> user.getPropertyValue(TELEGRAM_PROP_KEY)) .filter(Objects::nonNull) // looks like new Teamcity don't validate input with validator in user properties // so we should check input before send (TW-47469). It's fixed at bugtrack but looks like // it's still reproducing... .filter(TelegramNotificator::isLong) .map(Long::parseLong) .distinct() .collect(Collectors.toList()); } private Configuration createFreeMarkerConfig(@NotNull Path configDir) throws IOException { Configuration cfg = new Configuration(); cfg.setDefaultEncoding("UTF-8"); cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); cfg.setDirectoryForTemplateLoading(configDir.toFile()); cfg.setTemplateUpdateDelay(TeamCityProperties.getInteger( "teamcity.notification.template.update.interval", 60)); return cfg; } private static boolean isLong(@NotNull String value) { try { Long.parseLong(value); return true; } catch (NumberFormatException ex) { return false; } } }