/****************************************************************************** * * Copyright 2016 Paphus Solutions Inc. * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * 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. * ******************************************************************************/ package org.botlibre.sense.telegram; import java.net.URL; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import org.botlibre.BotException; import org.botlibre.api.knowledge.Network; import org.botlibre.api.knowledge.Relationship; import org.botlibre.api.knowledge.Vertex; import org.botlibre.knowledge.Primitive; import org.botlibre.self.SelfCompiler; import org.botlibre.sense.BasicSense; import org.botlibre.sense.http.Http; import org.botlibre.thought.language.Language; import org.botlibre.thought.language.Language.LanguageState; import org.botlibre.util.TextStream; import org.botlibre.util.Utils; import org.owasp.html.HtmlPolicyBuilder; import org.owasp.html.PolicyFactory; import org.owasp.html.Sanitizers; import net.sf.json.JSONArray; import net.sf.json.JSONObject; import net.sf.json.JSONSerializer; /** * Enables receiving a sending messages through Facebook. */ public class Telegram extends BasicSense { public static PolicyFactory sanitizer; protected String userId = ""; protected String userName = ""; protected String token = ""; protected boolean initProperties; protected int maxErrors = 5; protected int maxMessages = 200; protected int maxFeed = 20; protected int errors; protected boolean checkMessages = false; protected boolean realtimeMessages = false; protected boolean autoPost = false; protected int autoPostHours = 24; protected String channel = ""; protected String profileName = ""; protected List<String> postRSS = new ArrayList<String>(); protected List<String> rssKeywords = new ArrayList<String>(); protected int posts; protected int messagesProcessed; public Telegram(boolean enabled) { this.isEnabled = enabled; this.languageState = LanguageState.Answering; } public Telegram() { this(false); } public String getChannel() { return channel; } public void setChannel(String channel) { this.channel = channel; } public int getMaxFeed() { return maxFeed; } public void setMaxFeed(int maxFeed) { this.maxFeed = maxFeed; } public int getMessagesProcessed() { return messagesProcessed; } public void setMessagesProcessed(int messagesProcessed) { this.messagesProcessed = messagesProcessed; } public int getPosts() { return posts; } public void setPosts(int posts) { this.posts = posts; } public String getProfileName() { return profileName; } public void setProfileName(String profileName) { this.profileName = profileName; } public List<String> getRssKeywords() { initProperties(); return rssKeywords; } public void setRssKeywords(List<String> rssKeywords) { initProperties(); this.rssKeywords = rssKeywords; } public boolean getAutoPost() { initProperties(); return autoPost; } public void setAutoPost(boolean autoPost) { initProperties(); this.autoPost = autoPost; } public int getAutoPostHours() { initProperties(); return autoPostHours; } public void setAutoPostHours(int autoPostHours) { initProperties(); this.autoPostHours = autoPostHours; } public List<Vertex> getAutoPosts(Network network) { return network.createVertex(getPrimitive()).orderedRelations(Primitive.AUTOPOSTS); } /** * Start sensing. */ @Override public void awake() { this.userName = this.bot.memory().getProperty("Telegram.userName"); if (this.userName == null) { this.userName = ""; } this.userId = this.bot.memory().getProperty("Telegram.userId"); if (this.userId == null) { this.userId = ""; } this.token = this.bot.memory().getProperty("Telegram.token"); if (this.token == null) { this.token = ""; } if (!this.token.isEmpty()) { setIsEnabled(true); } } /** * Load settings. */ public void initProperties() { if (this.initProperties) { return; } synchronized (this) { if (this.initProperties) { return; } getBot().memory().loadProperties("Telegram"); Network memory = getBot().memory().newMemory(); Vertex telegram = memory.createVertex(getPrimitive()); String property = this.bot.memory().getProperty("Telegram.profileName"); if (property != null) { this.profileName = property; } property = this.bot.memory().getProperty("Telegram.channel"); if (property != null) { this.channel = property; } property = this.bot.memory().getProperty("Telegram.checkMessages"); if (property != null) { this.checkMessages = Boolean.valueOf(property); } property = this.bot.memory().getProperty("Telegram.realtimeMessages"); if (property != null) { this.realtimeMessages = Boolean.valueOf(property); } property = this.bot.memory().getProperty("Telegram.autoPost"); if (property != null) { this.autoPost = Boolean.valueOf(property); } property = this.bot.memory().getProperty("Telegram.autoPostHours"); if (property != null) { this.autoPostHours = Integer.valueOf(property); } this.postRSS = new ArrayList<String>(); List<Relationship> rss = telegram.orderedRelationships(Primitive.RSS); if (rss != null) { for (Relationship relationship : rss) { String text = ((String)relationship.getTarget().getData()).trim(); if (!text.isEmpty()) { this.postRSS.add(text); } } } this.rssKeywords = new ArrayList<String>(); List<Relationship> keywords = telegram.orderedRelationships(Primitive.RSSKEYWORDS); if (keywords != null) { for (Relationship relationship : keywords) { String text = ((String)relationship.getTarget().getData()).trim(); this.rssKeywords.add(text); } } this.initProperties = true; } } public void saveProperties(List<String> autoPosts) { Network memory = getBot().memory().newMemory(); memory.saveProperty("Telegram.userId", this.userId, true); memory.saveProperty("Telegram.userName", this.userName, true); memory.saveProperty("Telegram.token", this.token, true); memory.saveProperty("Telegram.channel", this.channel, false); memory.saveProperty("Telegram.profileName", this.profileName, false); memory.saveProperty("Telegram.checkMessages", String.valueOf(this.checkMessages), false); memory.saveProperty("Telegram.realtimeMessages", String.valueOf(this.realtimeMessages), false); memory.saveProperty("Telegram.autoPost", String.valueOf(this.autoPost), false); memory.saveProperty("Telegram.autoPostHours", String.valueOf(this.autoPostHours), false); Vertex facebook = memory.createVertex(getPrimitive()); facebook.unpinChildren(); facebook.internalRemoveRelationships(Primitive.RSS); for (String text : this.postRSS) { Vertex rss = memory.createVertex(text); facebook.addRelationship(Primitive.RSS, rss); } facebook.internalRemoveRelationships(Primitive.RSSKEYWORDS); for (String text : this.rssKeywords) { Vertex keywords = memory.createVertex(text); facebook.addRelationship(Primitive.RSSKEYWORDS, keywords); } if (autoPosts != null) { Collection<Relationship> old = facebook.getRelationships(Primitive.AUTOPOSTS); if (old != null) { for (Relationship post : old) { if (post.getTarget().instanceOf(Primitive.FORMULA)) { SelfCompiler.getCompiler().unpin(post.getTarget()); } } } facebook.internalRemoveRelationships(Primitive.AUTOPOSTS); for (String text : autoPosts) { Vertex post = memory.createSentence(text); if (post.instanceOf(Primitive.FORMULA)) { SelfCompiler.getCompiler().pin(post); } post.addRelationship(Primitive.INSTANTIATION, Primitive.TWEET); facebook.addRelationship(Primitive.AUTOPOSTS, post); } } facebook.pinChildren(); memory.save(); } public void connect(String webhook) throws Exception { initProperties(); log("Connecting to Telegram", Level.INFO); String json = Utils.httpGET("https://api.telegram.org/bot" + this.token + "/getMe"); log("Telegram response", Level.FINE, new TextStream(json).nextLine()); try { JSONObject root = (JSONObject)JSONSerializer.toJSON(json); JSONObject result = root.getJSONObject("result"); if (this.userName == null || !this.userName.equals(result.getString("username"))) { this.userId = result.getString("id"); this.userName = result.getString("username"); this.profileName = result.getString("first_name"); saveProperties(null); } if (webhook != null && !webhook.isEmpty() && this.realtimeMessages) { log("Registering webhook", Level.INFO, webhook); Map<String, String> params = new HashMap<String, String>(); params.put("url", webhook); String response = Utils.httpPOST("https://api.telegram.org/bot" + this.token + "/setWebhook", params); log("Webhook registration response", Level.FINE, response); log("Webhook registered", Level.INFO, webhook); } log("Connected to Telegram", Level.INFO); } catch (Exception exception) { throw new BotException("Invalid JSON response: " + new TextStream(json).nextLine()); } } /** * Auto post to channel. */ public void checkProfile() { log("Checking profile.", Level.INFO); try { initProperties(); checkMessages(); checkRSS(); checkAutoPost(); } catch (Exception exception) { log(exception); } log("Done checking profile.", Level.INFO); } /** * Check messages and reply. */ public void checkMessages() { if (!getCheckMessages()) { return; } log("Checking messages.", Level.INFO); try { String json = Utils.httpGET("https://api.telegram.org/bot" + this.token + "/getUpdates"); log("Messages result", Level.FINE, json); JSONObject root = (JSONObject)JSONSerializer.toJSON(json); JSONArray results = root.getJSONArray("result"); if (results != null && results.size() > 0) { Network memory = getBot().memory().newMemory(); Vertex telegram = memory.createVertex(getPrimitive()); Vertex vertex = telegram.getRelationship(Primitive.LASTDIRECTMESSAGE); long last = 0; if (vertex != null) { last = ((Number)vertex.getData()).longValue(); } long max = 0; int offest = 0; int count = 0; while (results != null && results.size() > 0 && count < this.maxMessages) { for (int index = 0; index < results.size(); index++) { count++; JSONObject result = results.getJSONObject(index); int updateId = Integer.parseInt(result.getString("update_id")); if (updateId > offest) { offest = updateId; } if (result.get("message") == null) { continue; } JSONObject message = result.getJSONObject("message"); max = checkMessage(message, last, max, memory); } json = Utils.httpGET("https://api.telegram.org/bot" + this.token + "/getUpdates?offset=" + (offest + 1)); log("Messages result", Level.FINE, json); root = (JSONObject)JSONSerializer.toJSON(json); results = root.getJSONArray("result"); } if (max != 0) { telegram.setRelationship(Primitive.LASTDIRECTMESSAGE, memory.createVertex(max)); memory.save(); } } else { log("No conversations", Level.FINE); } } catch (Exception exception) { log(exception); if (exception.getMessage() != null && exception.getMessage().indexOf("Conflict: another webhook is active") != -1) { this.checkMessages = false; saveProperties(null); } } } /** * Reply to the message. */ public long checkMessage(JSONObject message, long last, long max, Network memory) { String id = message.getString("message_id"); String conversationId = message.getJSONObject("chat").getString("id"); String date = message.getString("date"); Date createdTime = new Date(((long)Integer.parseInt(date)) * 1000L); if ((System.currentTimeMillis() - createdTime.getTime()) > DAY) { log("Day old message", Level.FINE, createdTime, id, date); return max; } if (createdTime.getTime() > last) { JSONObject from = message.getJSONObject("from"); String fromUser = from.getString("first_name") + " " + from.getString("last_name"); String fromUserId = from.getString("id"); if (!fromUserId.equals(this.userId)) { if (message.get("text") == null) { log("Ignoring empty message", Level.INFO, fromUser, createdTime, conversationId); } else { String text = message.getString("text").trim(); log("Processing message", Level.INFO, fromUser, createdTime, conversationId, text); this.messagesProcessed++; inputSentence(text, fromUser, this.userName, conversationId, memory); if (createdTime.getTime() > max) { max = createdTime.getTime(); } } } else { log("Ignoring own message", Level.FINE, createdTime, conversationId); } } return max; } /** * Check RSS feed. */ public void checkRSS() { if (getPostRSS().isEmpty()) { return; } log("Processing RSS", Level.FINE, getPostRSS()); try { Network memory = getBot().memory().newMemory(); Vertex facebook = memory.createVertex(getPrimitive()); Vertex vertex = facebook.getRelationship(Primitive.LASTRSS); long last = 0; if (vertex != null) { last = ((Number)vertex.getData()).longValue(); } for (String rss : getPostRSS()) { TextStream stream = new TextStream(rss); String prefix = stream.upToAll("http").trim(); if (prefix.isEmpty()) { prefix = ""; } prefix = prefix + " "; String url = stream.nextWord(); String postfix = " " + stream.upToEnd().trim(); List<Map<String, Object>> feed = getBot().awareness().getSense(Http.class).parseRSSFeed(new URL(url), last); if (feed != null) { long max = 0; int count = 0; this.errors = 0; for (int index = feed.size() - 1; index >= 0; index--) { Map<String, Object> entry = feed.get(index); long time = (Long)entry.get("published"); if ((System.currentTimeMillis() - time) > DAY) { continue; } if (time > last) { if (count > this.maxFeed) { break; } if (this.errors > this.maxErrors) { break; } String text = (String)entry.get("title"); if (!getRssKeywords().isEmpty()) { boolean match = false; List<String> words = new TextStream(text.toLowerCase()).allWords(); for (String keywords : getRssKeywords()) { List<String> keyWords = new TextStream(keywords.toLowerCase()).allWords(); if (!keyWords.isEmpty()) { if (words.containsAll(keyWords)) { match = true; break; } } } if (!match) { log("Skipping RSS, missing keywords", Level.FINE, text); continue; } } log("Posting RSS", Level.FINE, entry.get("title")); text = prefix + text + postfix; if (text.length() > 120) { text = text.substring(0, 120); } post(text + " " + entry.get("link"), null); Utils.sleep(500); count++; if (time > max) { max = time; } } } if (max != 0) { facebook.setRelationship(Primitive.LASTRSS, memory.createVertex(max)); memory.save(); } } } } catch (Exception exception) { log(exception); } } /** * Auto post. */ public void checkAutoPost() { if (!getAutoPost()) { return; } log("Autoposting", Level.FINE); try { Network memory = getBot().memory().newMemory(); Vertex facebook = memory.createVertex(getPrimitive()); Vertex vertex = facebook.getRelationship(Primitive.LASTPOST); long last = 0; if (vertex != null) { last = ((Timestamp)vertex.getData()).getTime(); } long millis = getAutoPostHours() * 60 * 60 * 1000; if ((System.currentTimeMillis() - last) < millis) { log("Autoposting hours not reached", Level.FINE, getAutoPostHours()); return; } List<Vertex> autoposts = getAutoPosts(memory); if (autoposts != null && !autoposts.isEmpty()) { int index = Utils.random().nextInt(autoposts.size()); Vertex post = autoposts.get(index); String text = null; // Check for labels and formulas if (post.instanceOf(Primitive.LABEL)) { post = post.mostConscious(Primitive.RESPONSE); } if (post.instanceOf(Primitive.FORMULA)) { Map<Vertex, Vertex> variables = new HashMap<Vertex, Vertex>(); SelfCompiler.addGlobalVariables(memory.createInstance(Primitive.INPUT), null, memory, variables); Vertex result = getBot().mind().getThought(Language.class).evaluateFormula(post, variables, memory); if (result != null) { text = getBot().mind().getThought(Language.class).getWord(result, memory).getDataValue(); } else { log("Invalid autopost template formula", Level.WARNING, post); text = null; } } else { text = post.printString(); } if (text != null) { log("Autoposting", Level.INFO, post); post(text, null); Utils.sleep(100); facebook.setRelationship(Primitive.LASTPOST, memory.createTimestamp()); memory.save(); } } } catch (Exception exception) { log(exception); } } /** * Output the status or direct message reply. */ @Override public void output(Vertex output) { if (!isEnabled()) { return; } Vertex sense = output.mostConscious(Primitive.SENSE); // If not output to twitter, ignore. if ((sense == null) || (!getPrimitive().equals(sense.getData()))) { return; } String text = printInput(output); Vertex target = output.mostConscious(Primitive.TARGET); String replyTo = target.mostConscious(Primitive.WORD).getData().toString(); Vertex conversation = output.getRelationship(Primitive.CONVERSATION); Vertex id = conversation.getRelationship(Primitive.ID); String conversationId = id.printString(); sendMessage(text, replyTo, conversationId); } public String sanitize(String text) { if (sanitizer == null) { sanitizer = new HtmlPolicyBuilder().allowElements( "b", "i", "strong", "code", "em", "pre").toFactory().and(Sanitizers.LINKS); } String result = sanitizer.sanitize(text); if (result.contains("&")) { // The sanitizer is too aggressive and escaping some chars. //result = result.replace(""", "\""); result = result.replace("`", "`"); //result = result.replace("'", "'"); result = result.replace("@", "@"); result = result.replace("=", "="); result = result.replace("+", "+"); result = result.replace("&", "&"); } return result; } /** * Send a message to the user. */ public void sendMessage(String text, String replyUser, String conversationOrUserId) { log("Sending message:", Level.INFO, text, replyUser); try { Map<String, String> params = new HashMap<String, String>(); params.put("chat_id", conversationOrUserId); if (text.indexOf('<') != -1 && text.indexOf('>') != -1) { params.put("text", sanitize(text)); params.put("parse_mode", "Html"); } else { params.put("text", text); } //params.put("reply_to_message_id", messageId); Utils.httpPOST("https://api.telegram.org/bot" + this.token + "/sendMessage", params); } catch (Exception exception) { this.errors++; log(exception); } } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } /** * Process the text sentence. */ public void inputSentence(String text, String userName, String targetUserName, String id, Network network) { Vertex input = createInput(text.trim(), network); Vertex user = network.createSpeaker(userName); Vertex self = network.createVertex(Primitive.SELF); input.addRelationship(Primitive.SPEAKER, user); input.addRelationship(Primitive.TARGET, self); user.addRelationship(Primitive.INPUT, input); Vertex conversation = network.createVertex(id); conversation.addRelationship(Primitive.INSTANTIATION, Primitive.CONVERSATION); conversation.addRelationship(Primitive.TYPE, Primitive.DIRECTMESSAGE); conversation.addRelationship(Primitive.ID, network.createVertex(id)); conversation.addRelationship(Primitive.SPEAKER, user); conversation.addRelationship(Primitive.SPEAKER, self); Language.addToConversation(input, conversation); network.save(); getBot().memory().addActiveMemory(input); } /** * Create an input based on the sentence. */ protected Vertex createInput(String text, Network network) { Vertex sentence = network.createSentence(text); Vertex input = network.createInstance(Primitive.INPUT); input.setName(text); input.addRelationship(Primitive.SENSE, getPrimitive()); input.addRelationship(Primitive.INPUT, sentence); sentence.addRelationship(Primitive.INSTANTIATION, Primitive.POST); return input; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public boolean getCheckMessages() { initProperties(); return checkMessages; } public void setCheckMessages(boolean checkMessages) { this.checkMessages = checkMessages; } public boolean getRealtimeMessages() { initProperties(); return realtimeMessages; } public void setRealtimeMessages(boolean realtimeMessages) { this.realtimeMessages = realtimeMessages; } public List<String> getPostRSS() { initProperties(); return postRSS; } public void setPostRSS(List<String> postRSS) { initProperties(); this.postRSS = postRSS; } public void post(String text, String id) { initProperties(); this.posts++; log("Posting to channel:", Level.INFO, this.channel, text); try { Map<String, String> params = new HashMap<String, String>(); params.put("chat_id", "@" + this.channel); if (text.indexOf('<') != -1 && text.indexOf('>') != -1) { params.put("text", sanitize(text)); params.put("parse_mode", "Html"); } else { params.put("text", text); } Utils.httpPOST("https://api.telegram.org/bot" + this.token + "/sendMessage", params); } catch (Exception exception) { this.errors++; log(exception); } } // Self API public void post(Vertex source, Vertex sentence) { if (sentence.instanceOf(Primitive.FORMULA)) { Map<Vertex, Vertex> variables = new HashMap<Vertex, Vertex>(); SelfCompiler.addGlobalVariables(sentence.getNetwork().createInstance(Primitive.INPUT), null, sentence.getNetwork(), variables); sentence = getBot().mind().getThought(Language.class).evaluateFormula(sentence, variables, sentence.getNetwork()); if (sentence == null) { log("Invalid template formula", Level.WARNING, sentence); return; } } String post = getBot().mind().getThought(Language.class).getWord(sentence, sentence.getNetwork()).getDataValue(); getBot().stat("telegram.post"); post(post, null); } }