1
0
mirror of https://github.com/chatopera/cosin.git synced 2025-06-16 18:30:03 +08:00
This commit is contained in:
Hai Liang Wang 2022-08-12 02:45:19 +01:00
parent 6facf37fb0
commit 25ff3a2052
19 changed files with 2432 additions and 7 deletions

View File

@ -1,10 +1,6 @@
# dev profile # dev profile
src/main/resources/application-dev.properties src/main/resources/application-dev.properties
# ignore channel views within plugins
src/main/resources/templates/admin/channel/*
!src/main/resources/templates/admin/channel/im
# ignore app views within plugins # ignore app views within plugins
src/main/resources/templates/apps/callout src/main/resources/templates/apps/callout
src/main/resources/templates/apps/callcenter src/main/resources/templates/apps/callcenter

View File

@ -1,3 +0,0 @@
plugins/*
!plugins/chatbot
!plugins/README.md

View File

@ -0,0 +1,210 @@
/*
* Copyright (C) 2019 Chatopera Inc, <https://www.chatopera.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chatopera.cc.plugins.messenger;
import com.chatopera.cc.basic.Constants;
import com.chatopera.cc.basic.MainContext;
import com.chatopera.cc.basic.MainUtils;
import com.chatopera.cc.controller.Handler;
import com.chatopera.cc.model.FbMessenger;
import com.chatopera.cc.model.Organ;
import com.chatopera.cc.model.SNSAccount;
import com.chatopera.cc.persistence.repository.FbMessengerRepository;
import com.chatopera.cc.persistence.repository.OrganRepository;
import com.chatopera.cc.persistence.repository.SNSAccountRepository;
import com.chatopera.cc.proxy.OrganProxy;
import com.chatopera.cc.util.Menu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Controller
@RequestMapping("/admin/messenger")
public class MessengerChannelController extends Handler {
private final static Logger logger = LoggerFactory.getLogger(MessengerChannelController.class);
@Autowired
private FbMessengerRepository fbMessengerRepository;
@Autowired
private OrganRepository organRepository;
@Autowired
private OrganProxy organProxy;
@Autowired
private SNSAccountRepository snsAccountRepository;
private Map<String, Organ> getOwnOrgan(HttpServletRequest request) {
return organProxy.findAllOrganByParentAndOrgi(super.getOrgan(request), super.getOrgi(request));
}
@RequestMapping("/index")
@Menu(type = "admin", subtype = "messenger")
public ModelAndView index(ModelMap map, HttpServletRequest request) {
Map<String, Organ> organs = getOwnOrgan(request);
List<FbMessenger> fbMessengers = fbMessengerRepository.findByOrganIn(organs.keySet());
Organ currentOrgan = super.getOrgan(request);
map.addAttribute("fbMessengers", fbMessengers);
map.addAttribute("organs", organs);
map.addAttribute("organ", currentOrgan);
return request(super.createView("/admin/channel/messenger/index"));
}
@RequestMapping("/add")
@Menu(type = "admin", subtype = "messenger")
public ModelAndView add(ModelMap map, HttpServletRequest request) {
Organ currentOrgan = super.getOrgan(request);
map.addAttribute("organ", currentOrgan);
return request(super.createView("/admin/channel/messenger/add"));
}
@RequestMapping("/save")
@Menu(type = "admin", subtype = "messenger")
public ModelAndView save(ModelMap map, HttpServletRequest request, @Valid FbMessenger fbMessenger) {
String msg = "save_ok";
Organ currentOrgan = super.getOrgan(request);
FbMessenger fbMessengerOne = fbMessengerRepository.findOneByPageId(fbMessenger.getPageId());
if (fbMessengerOne != null) {
msg = "save_no_PageId";
} else {
fbMessenger.setId(MainUtils.getUUID());
fbMessenger.setOrgan(currentOrgan.getId());
if (fbMessenger.getStatus() == null) {
fbMessenger.setStatus("disabled");
}
fbMessenger.setCreatetime(new Date());
fbMessenger.setUpdatetime(new Date());
fbMessenger.setAiid(null);
fbMessengerRepository.save(fbMessenger);
SNSAccount snsAccount = new SNSAccount();
snsAccount.setId(MainUtils.genID());
snsAccount.setCreatetime(new Date());
snsAccount.setOrgi(super.getOrgi(request));
snsAccount.setName(fbMessenger.getName());
snsAccount.setOrgan(currentOrgan.getId());
snsAccount.setSnsid(fbMessenger.getPageId());
snsAccount.setSnstype(MainContext.ChannelType.MESSENGER.toString());
snsAccountRepository.save(snsAccount);
}
return request(super.createView("redirect:/admin/messenger/index.html?msg=" + msg));
}
@RequestMapping("/edit")
@Menu(type = "admin", subtype = "messenger")
public ModelAndView edit(ModelMap map, HttpServletRequest request, @Valid String id) {
FbMessenger fbMessenger = fbMessengerRepository.findOne(id);
Organ fbOrgan = organRepository.getOne(fbMessenger.getOrgan());
map.addAttribute("organ", fbOrgan);
map.addAttribute("fb", fbMessenger);
return request(super.createView("/admin/channel/messenger/edit"));
}
@RequestMapping("/update")
@Menu(type = "admin", subtype = "messenger")
public ModelAndView update(ModelMap map, HttpServletRequest request, @Valid FbMessenger fbMessenger) {
String msg = "update_ok";
FbMessenger oldMessenger = fbMessengerRepository.findOne(fbMessenger.getId());
oldMessenger.setName(fbMessenger.getName());
if (fbMessenger.getStatus() != null) {
oldMessenger.setStatus(fbMessenger.getStatus());
} else {
oldMessenger.setStatus(Constants.MESSENGER_CHANNEL_DISABLED);
}
oldMessenger.setToken(fbMessenger.getToken());
oldMessenger.setVerifyToken(fbMessenger.getVerifyToken());
oldMessenger.setUpdatetime(new Date());
fbMessengerRepository.save(oldMessenger);
return request(super.createView("redirect:/admin/messenger/index.html?msg=" + msg));
}
@RequestMapping("/delete")
@Menu(type = "admin", subtype = "messenger")
public ModelAndView delete(ModelMap map, HttpServletRequest request, @Valid String id) {
String msg = "delete_ok";
FbMessenger fbMessenger = fbMessengerRepository.getOne(id);
fbMessengerRepository.delete(id);
snsAccountRepository.findBySnsid(fbMessenger.getPageId()).ifPresent(snsAccount -> {
snsAccountRepository.delete(snsAccount);
});
return request(super.createView("redirect:/admin/messenger/index.html?msg=" + msg));
}
@RequestMapping("/setting")
@Menu(type = "admin", subtype = "messenger")
public ModelAndView setting(ModelMap map, HttpServletRequest request, @Valid String id) {
FbMessenger fbMessenger = fbMessengerRepository.findOne(id);
Organ fbOrgan = organRepository.getOne(fbMessenger.getOrgan());
map.mergeAttributes(fbMessenger.parseConfigMap());
map.addAttribute("organ", fbOrgan);
map.addAttribute("fb", fbMessenger);
return request(super.createView("/admin/channel/messenger/setting"));
}
@RequestMapping(value = "/setting/save", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@Menu(type = "admin", subtype = "messenger")
public ModelAndView saveSetting(ModelMap map, HttpServletRequest request, @Valid String id, @RequestBody MultiValueMap<String, String> formData) {
String msg = "update_ok";
FbMessenger fbMessenger = fbMessengerRepository.findOne(id);
if (fbMessenger != null) {
fbMessenger.setConfigMap(formData.toSingleValueMap());
fbMessengerRepository.save(fbMessenger);
}
return request(super.createView("redirect:/admin/messenger/index.html?msg=" + msg));
}
@RequestMapping("/setStatus")
@Menu(type = "admin", subtype = "messenger")
@ResponseBody
public String setStatus(ModelMap map, HttpServletRequest request, @Valid String id, @Valid String status) {
FbMessenger fbMessenger = fbMessengerRepository.findOne(id);
fbMessenger.setStatus(status);
fbMessengerRepository.save(fbMessenger);
return "ok";
}
}

View File

@ -0,0 +1,90 @@
package com.chatopera.cc.plugins.messenger;
import com.chatopera.cc.basic.MainContext;
import com.chatopera.cc.model.AgentUser;
import com.chatopera.cc.model.OnlineUser;
import com.chatopera.cc.peer.PeerContext;
import com.chatopera.cc.persistence.repository.OnlineUserRepository;
import com.chatopera.cc.socketio.message.ChatMessage;
import com.chatopera.compose4j.Functional;
import com.chatopera.compose4j.Middleware;
import org.apache.commons.lang.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* Messenger 消息处理
*/
@Component
public class MessengerChannelMessager implements Middleware<PeerContext> {
private final static Logger logger = LoggerFactory.getLogger(MessengerChannelMessager.class);
@Autowired
private OnlineUserRepository onlineUserRes;
@Autowired
private MessengerMessageProxy messengerMessageProxy;
@Override
public void apply(final PeerContext ctx, final Functional next) {
if ((!ctx.isSent()) && ctx.getChannel() == MainContext.ChannelType.MESSENGER) {
AgentUser agentUser = ctx.getMessage().getAgentUser();
final OnlineUser onlineUser = onlineUserRes.findOneByUseridAndOrgi(
agentUser.getUserid(), agentUser.getOrgi());
if (onlineUser != null) {
handle(ctx, onlineUser);
} else {
logger.info(
"[apply] can not online user or its contactsid with agentUserId {}, userid {} and orgi {}",
agentUser.getId(), agentUser.getUserid(), agentUser.getOrgi());
}
}
next.apply();
}
/**
* 处理消息体
*
* @param ctx
* @param onlineUser
*/
private void handle(final PeerContext ctx, final OnlineUser onlineUser) {
if (ctx.getMessage().getChannelMessage() instanceof ChatMessage) {
final ChatMessage chatMessage = (ChatMessage) ctx.getMessage().getChannelMessage();
logger.info(
"[apply] chat message type {}, content {}", chatMessage.getMsgtype(),
chatMessage.getMessage());
if (StringUtils.equals(chatMessage.getMsgtype(), MainContext.MediaType.TEXT.toString())) {
Document document = Jsoup.parse(chatMessage.getMessage());
Elements pngs = document.select("img[src]");
if (pngs.size() > 0) {
for (Element element : pngs) {
String imgUrl = element.attr("src");
if (StringUtils.isNotBlank(imgUrl)) {
messengerMessageProxy.sendImage(onlineUser.getAppid(), onlineUser.getUserid(), imgUrl);
}
}
} else {
messengerMessageProxy.send(onlineUser.getAppid(), onlineUser.getUserid(), document.text());
}
} else if (StringUtils.equals(chatMessage.getMsgtype(), MainContext.MediaType.IMAGE.toString())) {
messengerMessageProxy.sendImage(onlineUser.getAppid(), onlineUser.getUserid(), chatMessage.getMessage());
}
logger.info("[apply] message is sent.");
ctx.setSent(true);
} else {
if (StringUtils.isNotBlank(ctx.getMessage().getMessage())) {
messengerMessageProxy.send(onlineUser.getAppid(), onlineUser.getUserid(), ctx.getMessage().getMessage());
}
}
}
}

View File

@ -0,0 +1,149 @@
package com.chatopera.cc.plugins.messenger;
import com.chatopera.bot.exception.ChatbotException;
import com.chatopera.cc.acd.ACDServiceRouter;
import com.chatopera.cc.basic.Constants;
import com.chatopera.cc.basic.MainContext;
import com.chatopera.cc.basic.MainUtils;
import com.chatopera.cc.exception.CSKefuException;
import com.chatopera.cc.model.*;
import com.chatopera.cc.persistence.repository.*;
import com.chatopera.cc.plugins.chatbot.ChatbotProxy;
import com.chatopera.cc.proxy.OnlineUserProxy;
import com.chatopera.cc.socketio.message.ChatMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.net.MalformedURLException;
import java.util.Date;
import java.util.Optional;
@Component
public class MessengerChatbot {
private final static Logger logger = LoggerFactory.getLogger(MessengerChatbot.class);
@Autowired
private ChatbotProxy chatbotProxy;
@Autowired
private AgentUserRepository agentUserRes;
@Autowired
private ChatMessageRepository chatMessageRes;
@Autowired
private SNSAccountRepository snsAccountRes;
@Autowired
private AgentServiceRepository agentServiceRes;
@Autowired
private MessengerMessageProxy messengerMessageProxy;
@Autowired
private OnlineUserRepository onlineUserRes;
public boolean receiveMessage(String chatbotId, String fromId, String snsID, OnlineUser onlineUser, MainContext.MediaType msgType, String msg) throws ChatbotException, MalformedURLException {
logger.info("[receiveMessage] message: chatbotId {},fromId {},toId {},msg {} ", chatbotId, fromId, snsID, msg);
// 在线客服访客咨询记录
Date now = new Date();
Optional<SNSAccount> optionalSNSAccount = snsAccountRes.findBySnsid(snsID);
SNSAccount snsAccount = optionalSNSAccount.get();
CousultInvite invite = OnlineUserProxy.consult(onlineUser.getAppid(), Constants.SYSTEM_ORGI);
AgentUser agentUser = agentUserRes.findOneByUseridAndStatusNotAndChannelAndOrgi(fromId, MainContext.AgentUserStatusEnum.END.toString(), MainContext.ChannelType.MESSENGER.toString(), Constants.SYSTEM_ORGI).orElseGet(() -> {
AgentUser au = new AgentUser(
onlineUser.getUserid(),
MainContext.ChannelType.MESSENGER.toString(),
onlineUser.getId(),
onlineUser.getUsername(),
Constants.SYSTEM_ORGI,
onlineUser.getAppid());
au.setServicetime(now);
au.setCreatetime(now);
au.setUpdatetime(now);
au.setLogindate(now);
au.setSessionid(onlineUser.getSessionid());
au.setRegion(onlineUser.getRegion());
au.setUsername(onlineUser.getUsername());
au.setSkill(snsAccount.getOrgan());
au.setAppid(snsAccount.getSnsid());
au.setOrgi(Constants.SYSTEM_ORGI);
au.setNickname(onlineUser.getUsername());
au.setStatus(MainContext.AgentUserStatusEnum.INSERVICE.toString());
// 聊天机器人处理的请求
au.setOpttype(MainContext.OptType.CHATBOT.toString());
au.setAgentno(chatbotId); // 聊天机器人ID
au.setAgentname(invite != null ? invite.getAiname() : "机器人客服");
au.setCity(onlineUser.getCity());
au.setProvince(onlineUser.getProvince());
au.setCountry(onlineUser.getCountry());
AgentService agentService = ACDServiceRouter.getAcdChatbotService().processChatbotService(
invite != null ? invite.getAiname() : "机器人客服", au, Constants.SYSTEM_ORGI);
au.setAgentserviceid(agentService.getId());
// 标记为机器人坐席
au.setChatbotops(true);
// 保存到MySQL
agentUserRes.save(au);
return au;
});
if (agentUser.isChatbotops()) {
ChatMessage data = new ChatMessage();
data.setMessage(msg);
data.setUserid(fromId);
data.setUsession(fromId); // 绑定唯一用户
data.setSessionid(agentUser.getSessionid());
data.setMessage(MainUtils.processEmoti(data.getMessage())); // 处理表情
data.setTouser(chatbotId);
data.setUsername(agentUser.getUsername());
data.setAiid(chatbotId);
data.setAgentserviceid(agentUser.getAgentserviceid());
data.setChannel(agentUser.getChannel());
data.setOrgi(Constants.SYSTEM_ORGI);
data.setContextid(agentUser.getAgentserviceid()); // 一定要设置 ContextID
data.setCalltype(MainContext.CallType.IN.toString());
data.setMsgtype(msgType.toString());
chatMessageRes.save(data);
if (MainContext.MediaType.TEXT == msgType) {
// 发送消息给Bot
chatbotProxy.publishMessage(data, Constants.CHATBOT_EVENT_TYPE_CHAT);
}
return true;
} else {
return false;
}
}
public void switchManualCustomerService(String fromId) {
agentUserRes.findOneByUseridAndStatusNotAndChannelAndOrgi(fromId, MainContext.AgentUserStatusEnum.END.toString(), MainContext.ChannelType.MESSENGER.toString(), Constants.SYSTEM_ORGI).ifPresent(agentUser -> {
if (agentUser.isChatbotops()) {
Date now = new Date();
AgentService agentService = agentServiceRes.findOne(agentUser.getAgentserviceid());
agentService.setStatus(MainContext.AgentUserStatusEnum.END.toString());
agentService.setEndtime(now);
agentServiceRes.save(agentService);
agentUser.setAgentserviceid(null);
agentUser.setChatbotops(false);
agentUser.setAgentno(null);
agentUserRes.save(agentUser);
snsAccountRes.findBySnsid(agentUser.getAppid()).ifPresent(snsAccount -> {
OnlineUser onlineUser = onlineUserRes.findOneByUseridAndOrgi(fromId, Constants.SYSTEM_ORGI);
try {
messengerMessageProxy.scheduleMessengerAgentUser(agentUser, onlineUser, snsAccount, Constants.SYSTEM_ORGI);
} catch (CSKefuException e) {
e.printStackTrace();
}
});
}
});
}
}

View File

@ -0,0 +1,119 @@
package com.chatopera.cc.plugins.messenger;
import com.alibaba.fastjson.JSONObject;
import com.chatopera.cc.basic.Constants;
import com.chatopera.cc.basic.MainContext;
import com.chatopera.cc.model.FbMessenger;
import com.chatopera.cc.model.OnlineUser;
import com.chatopera.cc.persistence.repository.FbMessengerRepository;
import com.chatopera.cc.persistence.repository.OnlineUserRepository;
import com.chatopera.cc.plugins.chatbot.ChatbotConstants;
import com.chatopera.cc.plugins.chatbot.ChatbotContext;
import com.chatopera.cc.socketio.message.ChatMessage;
import com.chatopera.compose4j.Functional;
import com.chatopera.compose4j.Middleware;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
public class MessengerChatbotMessager implements Middleware<ChatbotContext> {
private final static Logger logger = LoggerFactory.getLogger(MessengerChatbotMessager.class);
private final static String EVALUATION_YES_REPLY = "evaluationYesReply";
private final static String EVALUATION_NO_REPLY = "evaluationNoReply";
@Autowired
private MessengerMessageProxy messengerMessageProxy;
@Autowired
private OnlineUserRepository onlineUserRes;
@Autowired
private FbMessengerRepository fbMessengerRepository;
private final Map<String, String> messengerConfig = new HashMap<String, String>() {
{
put("transferManualService", "转人工");
put("suggestQuestion", "您是否想问以下问题");
put("evaluationAsk", "以上答案是否对您有帮助");
put("evaluationYes", "");
put("evaluationNo", "");
put(EVALUATION_YES_REPLY, "感谢您的反馈,我们会做的更好!");
put(EVALUATION_NO_REPLY, "感谢您的反馈,机器人在不断的学习!");
}
};
@Override
public void apply(final ChatbotContext ctx, final Functional next) {
ChatMessage resp = ctx.getResp();
if (MainContext.ChannelType.MESSENGER.toString().equals(resp.getChannel())) {
final OnlineUser onlineUser = onlineUserRes.findOneByUseridAndOrgi(
resp.getUserid(), Constants.SYSTEM_ORGI);
Map<String, String> configMap = messengerConfig;
FbMessenger fbMessenger = fbMessengerRepository.findOneByPageId(onlineUser.getAppid());
if (fbMessenger != null && StringUtils.isNotBlank(fbMessenger.getConfig())) {
configMap = (Map<String, String>) JSONObject.parse(fbMessenger.getConfig());
}
if (StringUtils.isNotBlank(resp.getExpmsg())) {
String jsonStr = processGenericTemplate(resp.getExpmsg(), configMap);
messengerMessageProxy.send(onlineUser.getAppid(), onlineUser.getUserid(), JSONObject.parseObject(jsonStr), fbMessenger);
} else {
messengerMessageProxy.send(onlineUser.getAppid(), onlineUser.getUserid(), processTextTemplate(resp.getMessage(), configMap));
}
}
next.apply();
}
/**
* 替换文本消息
*
* @param template
* @param params
* @return
*/
public static String processTextTemplate(String template, Map<String, String> params) {
if (StringUtils.equals(template, ChatbotConstants.PROVIDER_FEEDBACK_EVAL_POSITIVE_REPLY_PLACEHOLDER)) {
if (params.containsKey(EVALUATION_YES_REPLY) && StringUtils.isNotBlank(params.get(EVALUATION_YES_REPLY))) {
return params.get(EVALUATION_YES_REPLY);
}
} else if (StringUtils.equals(template, ChatbotConstants.PROVIDER_FEEDBACK_EVAL_NEGATIVE_REPLY_PLACEHOLDER)) {
if (params.containsKey(EVALUATION_NO_REPLY) && StringUtils.isNotBlank(params.get(EVALUATION_NO_REPLY))) {
return params.get(EVALUATION_NO_REPLY);
}
} else if (StringUtils.equals(template, "${leaveMeAlone}")) {
return "";
}
return template;
}
/**
* 替换模版消息
*
* @param template
* @param params
* @return
*/
public static String processGenericTemplate(String template, Map<String, String> params) {
Matcher m = Pattern.compile("\\$\\{\\w+\\}").matcher(template);
StringBuffer sb = new StringBuffer();
while (m.find()) {
String param = m.group();
Object value = params.get(param.substring(2, param.length() - 1));
m.appendReplacement(sb, value == null ? "" : value.toString());
}
m.appendTail(sb);
return sb.toString();
}
}

View File

@ -0,0 +1,65 @@
package com.chatopera.cc.plugins.messenger;
import com.alibaba.fastjson.JSONObject;
import com.chatopera.cc.basic.Constants;
import com.chatopera.cc.model.FbMessenger;
import com.chatopera.cc.model.FbOTN;
import com.chatopera.cc.model.FbOtnFollow;
import com.chatopera.cc.persistence.repository.FbMessengerRepository;
import com.chatopera.cc.persistence.repository.FbOTNFollowRepository;
import com.chatopera.cc.persistence.repository.FbOTNRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
@Component
public class MessengerEventSubscription {
private final static Logger logger = LoggerFactory.getLogger(MessengerEventSubscription.class);
@Autowired
private FbMessengerRepository fbMessengerRepository;
@Autowired
private FbOTNRepository otnRepository;
@Autowired
private FbOTNFollowRepository otnFollowRepository;
@Autowired
private MessengerMessageProxy messengerMessageProxy;
@JmsListener(destination = Constants.INSTANT_MESSAGING_MQ_QUEUE_FACEBOOK_OTN, containerFactory = "jmsListenerContainerQueue")
public void onPublish(final String jsonStr) {
JSONObject payload = JSONObject.parseObject(jsonStr);
String otnId = payload.getString("otnId");
Date sendtime = payload.getTimestamp("sendtime");
FbOTN otn = otnRepository.getOne(otnId);
FbMessenger fbMessenger = fbMessengerRepository.findOneByPageId(otn.getPageId());
if (fbMessenger != null && otn != null) {
if (otn.getStatus().equals("create") && otn.getSendtime() != null && otn.getSendtime().equals(sendtime)) {
otn.setStatus("sending");
otnRepository.save(otn);
}
if (otn.getStatus().equals("sending")) {
List<FbOtnFollow> follows = otnFollowRepository.findByOtnId(otn.getId());
for (FbOtnFollow f : follows) {
if (f.getSendtime() == null) {
messengerMessageProxy.sendOtnText(fbMessenger.getToken(), f.getOtnToken(), otn.getOtnMessage());
f.setSendtime(new Date());
otnFollowRepository.save(f);
}
}
otn.setStatus("finish");
otnRepository.save(otn);
}
}
}
}

View File

@ -0,0 +1,540 @@
/*
* Copyright (C) 2019 Chatopera Inc, <https://www.chatopera.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chatopera.cc.plugins.messenger;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.chatopera.bot.exception.ChatbotException;
import com.chatopera.cc.acd.ACDAgentService;
import com.chatopera.cc.acd.ACDVisitorDispatcher;
import com.chatopera.cc.acd.basic.ACDComposeContext;
import com.chatopera.cc.acd.basic.ACDMessageHelper;
import com.chatopera.cc.basic.Constants;
import com.chatopera.cc.basic.MainContext;
import com.chatopera.cc.basic.MainUtils;
import com.chatopera.cc.cache.Cache;
import com.chatopera.cc.exception.CSKefuException;
import com.chatopera.cc.model.*;
import com.chatopera.cc.peer.PeerSyncIM;
import com.chatopera.cc.persistence.blob.JpaBlobHelper;
import com.chatopera.cc.persistence.repository.*;
import com.chatopera.cc.socketio.message.ChatMessage;
import com.chatopera.cc.socketio.message.Message;
import com.chatopera.cc.util.HttpClientUtil;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Component
public class MessengerMessageProxy {
private final static Logger logger = LoggerFactory.getLogger(MessengerMessageProxy.class);
private final String FACEBOOK_MESSAGES_API = "https://graph.facebook.com/v2.6/me/messages";
@Autowired
private OnlineUserRepository onlineUserRes;
@Autowired
private AgentUserRepository agentUserRes;
@Autowired
private SNSAccountRepository snsAccountRes;
@Autowired
private ACDMessageHelper acdMessageHelper;
@Autowired
private ACDVisitorDispatcher acdVisitorDispatcher;
@Autowired
private AgentServiceRepository agentServiceRes;
@Autowired
private Cache cache;
@Autowired
private PeerSyncIM peerSyncIM;
@Autowired
private FbMessengerRepository fbMessengerRepository;
@Autowired
private ChatbotRepository chatbotRes;
@Autowired
private MessengerChatbot messengerChatbot;
@Autowired
private FbOTNRepository otnRepository;
@Autowired
private FbOTNFollowRepository otnFollowRepository;
@Autowired
private ACDAgentService acdAgentService;
@Autowired
private ChatMessageRepository chatMessageRes;
@Autowired
private JpaBlobHelper jpaBlobHelper;
@Autowired
private StreamingFileRepository streamingFileRes;
@Value("${uk.im.server.host}")
private String host;
public void acceptOTNReq(String fromId, String toId, String otnToken, String ref) {
FbOTN otn = otnRepository.findOne(ref);
if (otn != null) {
FbOtnFollow follow = new FbOtnFollow();
follow.setId(MainUtils.getUUID());
follow.setOtnId(ref);
follow.setUserId(fromId);
follow.setPageId(toId);
follow.setOtnToken(otnToken);
follow.setCreatetime(new Date());
follow.setUpdatetime(new Date());
otnFollowRepository.save(follow);
otnRepository.incOneSubNumById(otn.getId());
sendOTNContent(toId, fromId, JSONObject.parseObject(otn.getSuccessMessage()));
}
}
public void acceptMeLink(String fromId, String toId, String ref) {
FbOTN otn = otnRepository.findOne(ref);
if (otn != null) {
if (StringUtils.isNotBlank(otn.getPreSubMessage())) {
Object obj = JSON.parse(otn.getPreSubMessage());
if (obj instanceof JSONObject) {
JSONObject json = (JSONObject) obj;
sendOTNContent(toId, fromId, json);
} else if (obj instanceof JSONArray) {
JSONArray jsonArray = (JSONArray) obj;
sendOTNContent(toId, fromId, jsonArray.getJSONObject(0));
sendOTNContent(toId, fromId, jsonArray.getJSONObject(1));
}
otnRepository.incOneMelinkNumById(otn.getId());
}
if (StringUtils.isNotBlank(otn.getSubMessage())) {
sendOTNReq(toId, fromId, otn.getSubMessage(), ref);
}
}
}
public void accept(String fromId, String toId, MainContext.MediaType msgType, String msg) throws CSKefuException, ChatbotException, MalformedURLException {
Optional<SNSAccount> optionalSNSAccount = snsAccountRes.findBySnsid(toId);
if (!optionalSNSAccount.isPresent()) {
logger.warn("[handle] SnsAccount is null.");
return;
}
SNSAccount snsAccount = optionalSNSAccount.get();
Date now = new Date();
OnlineUser onlineUser = onlineUserRes.findOneByUseridAndOrgi(fromId, Constants.SYSTEM_ORGI);
if (onlineUser == null) {
onlineUser = new OnlineUser();
}
if (StringUtils.isBlank(onlineUser.getUserid())) {
Map<String, String> profile = getPersonName(toId, fromId);
// 创建新的Onlineuser
onlineUser.setId(MainUtils.getUUID());
onlineUser.setUpdatetime(now);
onlineUser.setUsername(profile.get("name"));
onlineUser.setHeadimgurl(profile.get("profile_pic"));
onlineUser.setCreatetime(now);
onlineUser.setLogintime(now);
onlineUser.setChannel(MainContext.ChannelType.MESSENGER.toString());
onlineUser.setAppid(snsAccount.getSnsid());
onlineUser.setStatus(MainContext.OnlineUserStatusEnum.ONLINE.toString());
onlineUser.setOrgi(Constants.SYSTEM_ORGI);
onlineUser.setUserid(fromId);
onlineUser.setUsertype(MainContext.OnlineUserType.MESSENGER.toString());
onlineUserRes.save(onlineUser);
} else if (cache.existBlackEntityByUserIdAndOrgi(onlineUser.getUserid(), Constants.SYSTEM_ORGI)) {
// 检查该访客是否被拉黑
logger.info("[handle] online user {} is in black list.", onlineUser.getId());
return;
}
Chatbot c = chatbotRes.findBySnsAccountIdentifierAndOrgi(toId, Constants.SYSTEM_ORGI);
if (c != null && c.isEnabled()) {
if (!StringUtils.equals(Constants.CHATBOT_HUMAN_FIRST, c.getWorkmode())) {
Boolean sendService = messengerChatbot.receiveMessage(c.getId(), fromId, toId, onlineUser, msgType, msg);
if (sendService) {
return;
}
}
} else if (c != null && !c.isEnabled() && StringUtils.equals(Constants.CHATBOT_CHATBOT_ONLY, c.getWorkmode())) {
return;
} else {
agentUserRes.findOneByUseridAndStatusNotAndChannelAndOrgi(fromId, MainContext.AgentUserStatusEnum.END.toString(), MainContext.ChannelType.MESSENGER.toString(), Constants.SYSTEM_ORGI).ifPresent(p -> {
if (p.isChatbotops()) {
messengerChatbot.switchManualCustomerService(fromId);
}
});
}
/**
* 得到OnlineUser后获取AgentUser
* 因为AgentUser是和OnlineUser关联的
*/
// 一个OnlineUser可以对应多个agentUser, 此处获得
AgentUser agentUser = agentUserRes.findOneByUseridAndStatusNotAndChannelAndOrgi(fromId, MainContext.AgentUserStatusEnum.END.toString(), MainContext.ChannelType.MESSENGER.toString(), Constants.SYSTEM_ORGI)
.orElseGet(() -> new AgentUser());
AgentService agentService;
if (StringUtils.isBlank(agentUser.getAgentserviceid())) {
FbMessenger fbMessenger = fbMessengerRepository.findOneByPageId(toId);
if (fbMessenger != null && StringUtils.equals(fbMessenger.getStatus(), Constants.MESSENGER_CHANNEL_DISABLED)) {
return;
}
// 没有加载到进行中的AgentUser创建一个新的
agentService = scheduleMessengerAgentUser(
agentUser, onlineUser, snsAccount, Constants.SYSTEM_ORGI).orElseThrow(
() -> new CSKefuException("Can not resolve AgentService Object."));
} else {
agentService = agentServiceRes.findOne(agentUser.getAgentserviceid());
}
/**
* 给客服发送消息
*/
if (agentUser != null && agentService != null) {
ChatMessage chatMessage = new ChatMessage();
Message outMessage = new Message();
chatMessage.setMessage(msg);
chatMessage.setOrgi(Constants.SYSTEM_ORGI);
chatMessage.setUsername(agentUser.getName());
chatMessage.setCalltype(MainContext.CallType.IN.toString());
if (StringUtils.isNotBlank(agentUser.getAgentno())) {
chatMessage.setTouser(agentUser.getUserid());
}
chatMessage.setChannel(MainContext.ChannelType.MESSENGER.toString());
chatMessage.setUsession(agentUser.getUserid());
chatMessage.setId(MainUtils.getUUID());
chatMessage.setContextid(agentUser.getContextid());
chatMessage.setUserid(agentUser.getUserid());
chatMessage.setUsession(agentUser.getUserid());
chatMessage.setAgentserviceid(agentUser.getAgentserviceid());
chatMessage.setUsername(agentUser.getUsername());
chatMessage.setMsgtype(msgType.toString());
outMessage.setMessageType(chatMessage.getMsgtype());
outMessage.setMessage(msg);
outMessage.setAttachmentid(chatMessage.getAttachmentid());
outMessage.setCalltype(MainContext.CallType.IN.toString());
outMessage.setContextid(agentUser.getContextid());
outMessage.setAgentUser(agentUser);
outMessage.setChannelMessage(chatMessage);
outMessage.setCreatetime(Constants.DISPLAY_DATE_FORMATTER.format(
chatMessage.getCreatetime()));
outMessage.setMessage(chatMessage.getMessage());
outMessage.setChannelMessage(chatMessage);
outMessage.setAgentUser(agentUser);
outMessage.setAgentService(agentService);
outMessage.setCalltype(
MainContext.CallType.IN.toString());
outMessage.setCreatetime(
Constants.DISPLAY_DATE_FORMATTER.format(
now));
// Notify customer service to refresh the page
if (StringUtils.isNotBlank(agentService.getAgentno())) {
peerSyncIM.send(MainContext.ReceiverType.AGENT,
MainContext.ChannelType.MESSENGER,
agentUser.getAppid(),
MainContext.MessageType.MESSAGE,
agentService.getAgentno(), outMessage, true);
} else {
chatMessageRes.save((chatMessage));
}
} else {
logger.info("[handle] agent user not found");
}
}
/**
* 创建新的AgentUser
*
* @param onlineUser 访客
* @param snsAccount 社交信息账号
* @param orgi 租户ID
* @return
*/
public Optional<AgentService> scheduleMessengerAgentUser(
final AgentUser agentUser,
final OnlineUser onlineUser,
final SNSAccount snsAccount,
final String orgi) throws CSKefuException {
if (agentUser == null) {
throw new CSKefuException("Invalid param for agentUser, should not be null.");
}
String channel = MainContext.ChannelType.MESSENGER.toString();
Date now = new Date();
agentUser.setUsername(onlineUser.getUsername());
agentUser.setSkill(snsAccount.getOrgan());
agentUser.setOrgi(orgi);
agentUser.setNickname(onlineUser.getUsername());
agentUser.setUserid(onlineUser.getUserid());
agentUser.setStatus(MainContext.AgentUserStatusEnum.END.toString());
agentUser.setLogindate(now);
agentUser.setServicetime(now);
agentUser.setCreatetime(now);
agentUser.setUpdatetime(now);
agentUser.setSessiontimes(System.currentTimeMillis() - now.getTime());
agentUser.setChannel(channel);
agentUser.setAppid(snsAccount.getSnsid());
agentUserRes.save(agentUser);
// 为访客安排坐席
ACDComposeContext ctx = acdMessageHelper.getComposeContextWithAgentUser(
agentUser, false, MainContext.ChatInitiatorType.USER.toString());
ctx.setOnlineUserHeadimgUrl(onlineUser.getHeadimgurl());
acdVisitorDispatcher.enqueue(ctx);
acdAgentService.notifyAgentUserProcessResult(ctx);
return Optional.ofNullable(ctx.getAgentService());
}
public Map<String, String> getPersonName(String pageId, String psid) {
Map<String, String> result = new HashMap<>();
FbMessenger fbMessenger = fbMessengerRepository.findOneByPageId(pageId);
if (fbMessenger != null) {
Map<String, Object> searchParams = new HashMap<>();
searchParams.put("access_token", fbMessenger.getToken());
searchParams.put("fields", "locale,first_name,last_name,profile_pic");
try {
String res = HttpClientUtil.doGet("https://graph.facebook.com/" + psid, searchParams);
JSONObject json = JSONObject.parseObject(res);
String firstName = json.getString("first_name");
String lastName = json.getString("last_name");
result.put("profile_pic", json.getString("profile_pic"));
saveProfilePic(result, json);
if (StringUtils.isNotBlank(firstName) && StringUtils.isNotBlank(lastName)) {
result.put("name", firstName + " " + lastName);
}
} catch (IOException e) {
logger.error("[messenger] 详情获取异常", e);
}
}
return result;
}
private void saveProfilePic(Map<String, String> result, JSONObject json) {
try {
String profile_pic = json.getString("profile_pic");
if (StringUtils.isBlank(profile_pic)) {
return;
}
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(profile_pic);
HttpResponse response = httpClient.execute(httpGet);
//获取Http响应的码 200
int startCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
if (startCode == 200 && entity != null) {
InputStream input = entity.getContent();
long size = entity.getContentLength();
String fileid = MainUtils.getUUID();
StreamingFile sf = new StreamingFile();
sf.setId(fileid);
sf.setName(fileid);
sf.setMime(entity.getContentType().getValue());
sf.setData(jpaBlobHelper.createBlob(input, size));
streamingFileRes.save(sf);
String fileURL = "/res/image.html?id=" + fileid;
result.put("profile_pic", fileURL);
}
} catch (IOException exception) {
}
}
public void sendOTNContent(String fromId, String toId, JSONObject json) {
if (json.getString("type").equals("image")) {
sendImage(fromId, toId, json.getString("url"));
} else if (json.getString("type").equals("text") && StringUtils.isNotBlank(json.getString("content"))) {
send(fromId, toId, json.getString("content"));
}
}
public void sendOtnText(String fbToken, String otnToken, String msg) {
logger.info("send messenger to otnToken:{} msg:{}", otnToken, msg);
JSONObject inputJson = JSONObject.parseObject(msg);
try {
JSONObject json = new JSONObject();
JSONObject recipient = new JSONObject();
recipient.put("one_time_notif_token", otnToken);
JSONObject message = new JSONObject();
if (inputJson.getString("type").equals("image")) {
JSONObject attachment = new JSONObject();
attachment.put("type", "image");
JSONObject payload = new JSONObject();
payload.put("url", "https://" + host + inputJson.getString("url"));
attachment.put("payload", payload);
message.put("attachment", attachment);
json.put("recipient", recipient);
json.put("message", message);
} else {
message.put("text", inputJson.getString("content"));
message.put("metadata", "DEVELOPER_DEFINED_METADATA");
json.put("recipient", recipient);
json.put("message", message);
}
String result = HttpClientUtil.doPost(FACEBOOK_MESSAGES_API + "?access_token=" + fbToken, json.toJSONString());
logger.info(result);
} catch (IOException e) {
logger.error("[messenger] 发送消息异常", e);
}
}
public void send(String fromId, String toId, JSONObject message) {
send(fromId, toId, message, null);
}
public void send(String fromId, String toId, JSONObject message, FbMessenger fbMessenger) {
if (fbMessenger == null) {
fbMessenger = fbMessengerRepository.findOneByPageId(fromId);
}
if (fbMessenger != null) {
try {
JSONObject json = new JSONObject();
JSONObject recipient = new JSONObject();
recipient.put("id", toId);
json.put("recipient", recipient);
json.put("message", message);
String result = HttpClientUtil.doPost(FACEBOOK_MESSAGES_API + "?access_token=" + fbMessenger.getToken(), json.toJSONString());
logger.info(result);
} catch (IOException e) {
logger.error("[messenger] 发送消息异常", e);
}
}
}
public void sendImage(String fromId, String toId, String imageUrl) {
logger.info("send messenger fromId:{} toId:{} image:{}", fromId, toId, imageUrl);
JSONObject message = new JSONObject();
JSONObject attachment = new JSONObject();
attachment.put("type", "image");
JSONObject payload = new JSONObject();
if (StringUtils.indexOf(imageUrl, "http") > -1) {
payload.put("url", imageUrl);
} else {
payload.put("url", "https://" + host + imageUrl);
}
attachment.put("payload", payload);
message.put("attachment", attachment);
send(fromId, toId, message);
}
public void send(String fromId, String toId, String msg) {
logger.info("send messenger fromId:{} toId:{} msg:{}", fromId, toId, msg);
JSONObject message = new JSONObject();
message.put("text", StringEscapeUtils.unescapeHtml(msg));
send(fromId, toId, message);
}
public void sendOTNReq(String fromId, String toId, String title, String ref) {
logger.info("send messenger fromId:{} toId:{} title:{}", fromId, toId, title);
FbMessenger fbMessenger = fbMessengerRepository.findOneByPageId(fromId);
if (fbMessenger != null) {
try {
JSONObject json = new JSONObject();
JSONObject recipient = new JSONObject();
recipient.put("id", toId);
JSONObject message = new JSONObject();
JSONObject attachment = new JSONObject();
attachment.put("type", "template");
JSONObject payload = new JSONObject();
payload.put("template_type", "one_time_notif_req");
payload.put("title", title);
payload.put("payload", ref);
attachment.put("payload", payload);
message.put("attachment", attachment);
json.put("recipient", recipient);
json.put("message", message);
String result = HttpClientUtil.doPost(FACEBOOK_MESSAGES_API + "?access_token=" + fbMessenger.getToken(), json.toJSONString());
logger.info(result);
} catch (IOException e) {
logger.error("[messenger] 发送消息异常", e);
}
}
}
}

View File

@ -0,0 +1,203 @@
package com.chatopera.cc.plugins.messenger;
import com.alibaba.fastjson.JSONObject;
import com.chatopera.cc.activemq.BrokerPublisher;
import com.chatopera.cc.basic.Constants;
import com.chatopera.cc.basic.MainUtils;
import com.chatopera.cc.controller.Handler;
import com.chatopera.cc.controller.api.request.RestUtils;
import com.chatopera.cc.exception.CSKefuException;
import com.chatopera.cc.model.*;
import com.chatopera.cc.persistence.repository.FbMessengerRepository;
import com.chatopera.cc.persistence.repository.FbOTNRepository;
import com.chatopera.cc.proxy.AgentProxy;
import com.chatopera.cc.proxy.OrganProxy;
import com.chatopera.cc.util.Menu;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Controller
@RequestMapping("/apps/messenger/otn")
public class MessengerOTNController extends Handler {
@Autowired
private FbMessengerRepository fbMessengerRepository;
@Autowired
private FbOTNRepository otnRepository;
@Autowired
private OrganProxy organProxy;
@Autowired
private AgentProxy agentProxy;
@Autowired
private BrokerPublisher brokerPublisher;
private Map<String, Organ> getOwnOrgan(HttpServletRequest request) {
return organProxy.findAllOrganByParentAndOrgi(super.getOrgan(request), super.getOrgi(request));
}
@RequestMapping("/index")
@Menu(type = "admin", subtype = "messenger")
public ModelAndView index(ModelMap map, HttpServletRequest request, @Valid String queryPageId) {
Map<String, Organ> organs = getOwnOrgan(request);
List<FbMessenger> fbMessengers = fbMessengerRepository.findByOrganIn(organs.keySet());
List<String> pageIds = fbMessengers.stream().map(p -> p.getPageId()).collect(Collectors.toList());
map.addAttribute("fbMessengers", fbMessengers);
if (StringUtils.isNotBlank(queryPageId)) {
map.addAttribute("queryPageId", queryPageId);
pageIds = Arrays.asList(queryPageId);
}
Page<FbOTN> otns = otnRepository.findByPageIdIn(pageIds, new PageRequest(super.getP(request), super.getPs(request), Sort.Direction.DESC, "createtime"));
map.addAttribute("otns", otns);
return request(super.createView("/admin/channel/messenger/otn/index"));
}
@RequestMapping("/add")
@Menu(type = "admin", subtype = "messenger")
public ModelAndView add(ModelMap map, HttpServletRequest request) {
Map<String, Organ> organs = getOwnOrgan(request);
List<FbMessenger> fbMessengers = fbMessengerRepository.findByOrganIn(organs.keySet());
map.addAttribute("fbMessengers", fbMessengers);
return request(super.createView("/admin/channel/messenger/otn/add"));
}
@RequestMapping("/save")
@Menu(type = "admin", subtype = "messenger")
public ModelAndView save(ModelMap map, @Valid FbOTN otn, HttpServletRequest request) {
String msg = "save_ok";
otn.setId(MainUtils.getUUID());
otn.setCreatetime(new Date());
otn.setUpdatetime(new Date());
otn.setSubNum(0);
otn.setMelinkNum(0);
otn.setStatus("create");
otnRepository.save(otn);
if (otn.getSendtime() != null) {
delaySend(otn);
}
return request(super.createView("redirect:/apps/messenger/otn/index.html?msg=" + msg));
}
@RequestMapping("/edit")
@Menu(type = "admin", subtype = "messenger")
public ModelAndView edit(ModelMap map, @Valid String id, HttpServletRequest request) {
map.addAttribute("otn", otnRepository.getOne(id));
return request(super.createView("/admin/channel/messenger/otn/edit"));
}
@RequestMapping("/update")
@Menu(type = "admin", subtype = "messenger")
public ModelAndView update(ModelMap map, @Valid FbOTN otn, HttpServletRequest request) {
String msg = "update_ok";
FbOTN oldOtn = otnRepository.findOne(otn.getId());
if (oldOtn != null) {
Date oldSendtime = oldOtn.getSendtime();
oldOtn.setName(otn.getName());
oldOtn.setPreSubMessage(otn.getPreSubMessage());
oldOtn.setSubMessage(otn.getSubMessage());
oldOtn.setOtnMessage(otn.getOtnMessage());
oldOtn.setSuccessMessage(otn.getSuccessMessage());
oldOtn.setSendtime(otn.getSendtime());
oldOtn.setUpdatetime(new Date());
otnRepository.save((oldOtn));
if (otn.getSendtime() != null && !otn.getSendtime().equals(oldSendtime)) {
delaySend(otn);
}
}
return request(super.createView("redirect:/apps/messenger/otn/index.html?msg=" + msg));
}
private void delaySend(FbOTN otn) {
long delaySeconds = (otn.getSendtime().getTime() - new Date().getTime()) / 1000;
JSONObject payload = new JSONObject();
payload.put("otnId", otn.getId());
payload.put("sendtime", otn.getSendtime());
brokerPublisher.send(Constants.INSTANT_MESSAGING_MQ_QUEUE_FACEBOOK_OTN, payload.toJSONString(), false, (int) delaySeconds);
}
@RequestMapping("/send")
@Menu(type = "admin", subtype = "faceb ook")
public ModelAndView send(ModelMap map, @Valid String id, HttpServletRequest request) {
String msg = "send_ok";
FbOTN otn = otnRepository.getOne(id);
FbMessenger fbMessenger = fbMessengerRepository.findOneByPageId(otn.getPageId());
if (fbMessenger != null && otn != null && otn.getStatus().equals("create")) {
otn.setStatus("sending");
otn.setSendtime(new Date());
otnRepository.save(otn);
JSONObject payload = new JSONObject();
payload.put("otnId", otn.getId());
brokerPublisher.send(Constants.INSTANT_MESSAGING_MQ_QUEUE_FACEBOOK_OTN, payload.toJSONString());
}
return request(super.createView("redirect:/apps/messenger/otn/index.html?msg=" + msg));
}
@RequestMapping("/image/upload")
@Menu(type = "admin", subtype = "image", access = false)
public ResponseEntity<String> upload(
ModelMap map,
HttpServletRequest request,
@RequestParam(value = "imgFile", required = false) MultipartFile multipart
) throws IOException {
final User logined = super.getUser(request);
JSONObject result = new JSONObject();
HttpHeaders headers = RestUtils.header();
if (multipart != null && multipart.getOriginalFilename().lastIndexOf(".") > 0) {
try {
StreamingFile sf = agentProxy.saveFileIntoMySQLBlob(logined, multipart);
result.put("error", 0);
result.put("url", sf.getFileUrl());
} catch (CSKefuException e) {
result.put("error", 1);
result.put("message", "请选择文件");
}
} else {
result.put("error", 1);
result.put("message", "请选择图片文件");
}
return new ResponseEntity<>(result.toString(), headers, HttpStatus.OK);
}
@RequestMapping("/delete")
@Menu(type = "admin", subtype = "messenger")
public ModelAndView delete(ModelMap map, HttpServletRequest request, @Valid String id) {
String msg = "delete_ok";
otnRepository.delete(id);
return request(super.createView("redirect:/apps/messenger/otn/index.html?msg=" + msg));
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (C) 2019 Chatopera Inc, <https://www.chatopera.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chatopera.cc.plugins.messenger;
import com.chatopera.cc.basic.plugins.AbstractPluginConfigurer;
import com.chatopera.cc.basic.plugins.PluginRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
/**
* 定义Plugin存在
*/
@Configuration
public class MessengerPluginConfigurer extends AbstractPluginConfigurer {
private final static Logger logger = LoggerFactory.getLogger(MessengerPluginConfigurer.class);
private final static String pluginName = "Messenger 渠道";
private final static String pluginId = "messenger";
@Autowired
private PluginRegistry pluginRegistry;
@PostConstruct
public void setup() {
pluginRegistry.addPlugin(this);
}
@Override
public String getPluginId() {
return pluginId;
}
/**
* 获取消息服务的Bean的名字
* 当该方法存在时加载到消息处理的调用栈 PeerSyncIM
*
* @return
*/
@Override
public String getPluginName() {
return pluginName;
}
@Override
public String getIOEventHandler() {
return null;
}
@Override
public boolean isModule() {
return true;
}
}

View File

@ -0,0 +1,143 @@
/*
* Copyright (C) 2019 Chatopera Inc, <https://www.chatopera.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chatopera.cc.plugins.messenger;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSONPath;
import com.chatopera.bot.exception.ChatbotException;
import com.chatopera.cc.basic.MainContext;
import com.chatopera.cc.controller.Handler;
import com.chatopera.cc.exception.CSKefuException;
import com.chatopera.cc.model.FbMessenger;
import com.chatopera.cc.persistence.repository.FbMessengerRepository;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.net.MalformedURLException;
@Controller
@RequestMapping("/messenger")
public class MessengerWebhookChannelController extends Handler {
private final static Logger logger = LoggerFactory.getLogger(MessengerWebhookChannelController.class);
@Autowired
private FbMessengerRepository fbMessengerRepository;
@Autowired
private MessengerMessageProxy messengerMessageProxy;
@Autowired
private MessengerChatbot messengerChatbot;
@RequestMapping(value = "/webhook/{pageId}", method = RequestMethod.GET)
@ResponseBody
public String get(@PathVariable("pageId") String pageId, @RequestParam("hub.challenge") String challenge, @RequestParam("hub.verify_token") String verify_token) {
logger.info("[get] verify token pageid {}", pageId);
FbMessenger fbMessenger = fbMessengerRepository.findOneByPageId(pageId);
String result = "not allow!";
if (fbMessenger != null && fbMessenger.getVerifyToken().equals(verify_token)) {
result = challenge;
}
return result;
}
@RequestMapping(value = "/webhook/{pageId}", method = RequestMethod.POST)
@ResponseBody
public String post(@PathVariable("pageId") String pageId, @RequestBody JSONObject jsonParam) {
logger.info("[post] pageId {}, payload {}", pageId, jsonParam.toString());
// NOTE: 此处 PathVariable pageId 不安全不建议在函数内使用详见 #1190
// https://gitlab.chatopera.com/cskefu/cskefu.io/issues/1190
// 该值可使用 recipientId 更为准确recipientId 就是实际的 pageId
if (jsonParam.getString("object").equals("page")) {
JSONArray entries = jsonParam.getJSONArray("entry");
for (int i = 0; i < entries.size(); i++) {
JSONObject entry = entries.getJSONObject(i);
JSONArray messaging = entry.getJSONArray("messaging");
for (int j = 0; j < messaging.size(); j++) {
try {
messageHandler(messaging.getJSONObject(j));
} catch (Exception e) {
logger.error("[messenger] 接收消息异常", e);
}
}
}
}
return "";
}
private void messageHandler(JSONObject messagingEvent) throws ChatbotException, CSKefuException, MalformedURLException {
String senderId = (String) JSONPath.eval(messagingEvent, "$.sender.id");
String recipientId = (String) JSONPath.eval(messagingEvent, "$.recipient.id");
String referralType = (String) JSONPath.eval(messagingEvent, "$.referral.type");
String optinType = (String) JSONPath.eval(messagingEvent, "$.optin.type");
String postbackPayload = (String) JSONPath.eval(messagingEvent, "$.postback.payload");
JSONObject quickRepliesPayload = (JSONObject) JSONPath.eval(messagingEvent, "$.message.quick_reply");
if (StringUtils.equals(referralType, "OPEN_THREAD")) {
String ref = (String) JSONPath.eval(messagingEvent, "$.referral.ref");
messengerMessageProxy.acceptMeLink(senderId, recipientId, ref);
return;
} else if (StringUtils.equals(optinType, "one_time_notif_req")) {
String otnToken = (String) JSONPath.eval(messagingEvent, "$.optin.one_time_notif_token");
String ref = (String) JSONPath.eval(messagingEvent, "$.optin.payload");
messengerMessageProxy.acceptOTNReq(senderId, recipientId, otnToken, ref);
return;
} else if (StringUtils.isNotBlank(postbackPayload)) {
if (StringUtils.equals(postbackPayload, "TRANSFER_LABOR")) {
messengerChatbot.switchManualCustomerService(senderId);
} else if (StringUtils.equals(postbackPayload, "FAQ_LIST")) {
String msg = (String) JSONPath.eval(messagingEvent, "$.postback.title");
messengerMessageProxy.accept(senderId, recipientId, MainContext.MediaType.TEXT, msg);
} else if (StringUtils.equals(postbackPayload, "startChatopera")) {
messengerMessageProxy.accept(senderId, recipientId, MainContext.MediaType.TEXT, "__kickoff");
} else {
messengerMessageProxy.accept(senderId, recipientId, MainContext.MediaType.TEXT, postbackPayload);
}
return;
} else if (quickRepliesPayload != null) {
String msg = (String) JSONPath.eval(messagingEvent, "$.message.quick_reply.payload");
messengerMessageProxy.accept(senderId, recipientId, MainContext.MediaType.TEXT, msg);
return;
}
String msg = (String) JSONPath.eval(messagingEvent, "$.message.text");
JSONArray attachments = (JSONArray) JSONPath.eval(messagingEvent, "$.message.attachments");
if (StringUtils.isNotBlank(msg)) {
messengerMessageProxy.accept(senderId, recipientId, MainContext.MediaType.TEXT, msg);
} else {
for (Object att : attachments) {
String type = (String) JSONPath.eval(att, "$.type");
String url = (String) JSONPath.eval(att, "$.payload.url");
if (StringUtils.equals(type, "image")) {
messengerMessageProxy.accept(senderId, recipientId, MainContext.MediaType.IMAGE, url);
}
}
}
}
}

View File

@ -0,0 +1,59 @@
.uk-layui-form(style="margin:20px auto")
form.layui-form(action="/admin/messenger/save.html", method="post")
.layui-form-item
.layui-inline
label.layui-form-label(style="width:150px;")
| 技能组
.layui-input-inline
input.layui-input(style="width: 200px;", type="text", name="name", required="", lay-verify="required", autocomplete="off", value="#{organ.name}", disabled="disabled")
i.csfont(style='position: absolute;right: 3px;top: 8px;font-size: 20px;color: #e6e6e6') &#xe67d;
.layui-form-mid.layui-word-aux
| &nbsp;&nbsp;&nbsp;该渠道的人工服务由哪个技能组接待,默认为当前技能组,在右上角切换
.layui-form-item
.layui-inline
label.layui-form-label(style="width:150px;")
| 名称
font(color='red') *
.layui-input-inline
input.layui-input(style="width: 200px;", type="text", name="name", required="", lay-verify="required", autocomplete="off", placeholder="北美玩家页")
.layui-form-mid.layui-word-aux
| &nbsp;&nbsp;&nbsp;使用字符串命名该渠道
.layui-form-item
.layui-inline
label.layui-form-label(style="width:150px;")
| Page ID
font(color='red') *
.layui-input-inline
input.layui-input(style="width: 200px;", type="text", name="pageId", required="", lay-verify="required", autocomplete="off", placeholder="e.g. 1541840459")
.layui-form-mid.layui-word-aux
| &nbsp;&nbsp;&nbsp;Facebook Messenger 设置页面获得 Page Id
.layui-form-item
.layui-inline
label.layui-form-label(style="width:150px;")
| Access Token
font(color='red') *
.layui-input-inline
input.layui-input(style="width: 200px;", type="password", name="token", required="", lay-verify="required", autocomplete="off", placeholder="*********")
.layui-form-mid.layui-word-aux
| &nbsp;&nbsp;&nbsp;Facebook Messenger 设置页面获得 Access Token
.layui-form-item
.layui-inline
label.layui-form-label(style="width:150px;")
| Verify Token
font(color='red') *
.layui-input-inline
input.layui-input(style="width: 200px;", type="text", name="verifyToken", required="", lay-verify="required", autocomplete="off", placeholder="e.g. uN26itM4Ec")
.layui-form-mid.layui-word-aux
| &nbsp;&nbsp;&nbsp;自定义,并且回填到 Facebook Messenger 设置页面
.layui-form-button
.layui-button-block
button.layui-btn(lay-submit="", lay-filter="formDemo") 提交
button.layui-btn.layui-btn-original(type="reset") 重置
script.
layui.use('form', function () {
var form = layui.form();
form.render(); //更新全部
});
layui.use('element', function () {
var element = layui.element();
});

View File

@ -0,0 +1,61 @@
.uk-layui-form(style="margin:20px auto")
form.layui-form(action="/admin/messenger/update.html", method="post")
input(type="hidden", name="id", value=fb.id)
input(type="hidden", name="status", value=fb.status)
.layui-form-item
.layui-inline
label.layui-form-label(style="width:150px;")
| 技能组
.layui-input-inline
input.layui-input(style="width: 200px;", type="text", name="name", required="", lay-verify="required", autocomplete="off", value=organ.name, disabled="disabled")
i.csfont(style='position: absolute;right: 3px;top: 8px;font-size: 20px;color: #e6e6e6') &#xe67d;
.layui-form-mid.layui-word-aux
| &nbsp;&nbsp;&nbsp;该渠道的人工服务由哪个技能组接待,默认为当前技能组,在右上角切换
.layui-form-item
.layui-inline
label.layui-form-label(style="width:150px;")
| 名称
font(color='red') *
.layui-input-inline
input.layui-input(style="width: 200px;", type="text", name="name", required="", lay-verify="required", autocomplete="off", value=fb.name)
.layui-form-mid.layui-word-aux
| &nbsp;&nbsp;&nbsp;使用字符串命名该渠道
.layui-form-item
.layui-inline
label.layui-form-label(style="width:150px;")
| Page ID
font(color='red') *
.layui-input-inline
input.layui-input(style="width: 200px;", disabled ,type="text", name="pageId", required="", lay-verify="required", autocomplete="off", value=fb.pageId)
.layui-form-mid.layui-word-aux
| &nbsp;&nbsp;&nbsp;Facebook Messenger 设置页面获得 Page Id
.layui-form-item
.layui-inline
label.layui-form-label(style="width:150px;")
| Access Token
font(color='red') *
.layui-input-inline
input.layui-input(style="width: 200px;", type="password", name="token", required="", lay-verify="required", autocomplete="off", value=fb.token)
.layui-form-mid.layui-word-aux
| &nbsp;&nbsp;&nbsp;Facebook Messenger 设置页面获得 Access Token
.layui-form-item
.layui-inline
label.layui-form-label(style="width:150px;")
| Verify Token
font(color='red') *
.layui-input-inline
input.layui-input(style="width: 200px;", type="text", name="verifyToken", required="", lay-verify="required", autocomplete="off", value=fb.verifyToken)
.layui-form-mid.layui-word-aux
| &nbsp;&nbsp;&nbsp;自定义,并且回填到 Facebook Messenger 设置页面
.layui-form-button
.layui-button-block
button.layui-btn(lay-submit="", lay-filter="formDemo") 提交
button.layui-btn.layui-btn-original(type="reset") 重置
script.
layui.use('form', function () {
var form = layui.form();
form.render(); //更新全部
});
layui.use('element', function () {
var element = layui.element();
});

View File

@ -0,0 +1,17 @@
.row(style="border-bottom: 10px solid #EFEFEF;padding:10px;")
.col-lg-8
.ukefu-measure
.ukefu-bt
.layui-icon.ukewo-btn.ukefu-content-ind
img(src="/images/messenger.png", style="width:60px;height:60px;")
.ukefu-bt-text
.ukefu-bt-text-title(style="font-weight:400;font-size:24px;border-bottom:1px solid #dedede;") #{fb.name}
span(style="font-size:15px;color:#AAAAAA;") &nbsp;接入渠道创建时间:#{pugHelper.formatDate('yyyy-MM-dd HH:mm:ss', fb.createtime)}
.ukefu-bt-text-content(style="") Facebook Page Id: #{fb.pageId}
.ukefu-tab-title(style="margin-left: 0px;")
ul.tab-title
.ukefu-tab-title(style="margin-left: 0px;")
ul.tab-title
li(class={'layui-this': subtype == 'messenger'})
a(href="/admin/messenger/setting.html?id=" + fb.id) 基本设置

View File

@ -0,0 +1,176 @@
extends /admin/include/layout.pug
block content
.row
input#copyInput(type="hidden")
#me-config.col-lg-12
h1.site-h1(style="background-color:#FFFFFF;")
| Messenger 列表 (#{size(fbMessengers)})
span(style="float:right;")
.layui-btn-group
if organ.skill == true
button.layui-btn.layui-btn-small.green(href="/admin/messenger/add.html", data-toggle="ajax", data-width="800", data-height="400", data-title="创建 Messenger 渠道") 创建渠道
else
button.layui-btn.layui-btn-disabled.layui-btn-small.layui-btn-warm(data-toggle="tooltip" title="当前组织机构非技能组,不支持创建 Messenger 渠道,使用右上角菜单切换") 创建渠道
button.layui-btn.layui-btn-small.layui-btn-normal(onclick='openExternalUrlWithBlankTarget("https://docs.chatopera.com/products/cskefu/channels/messenger/index.html")') 文档中心
.row(style="padding:5px;")
blockquote.layui-elem-quote.layui-quote-nm
i.layui-icon(style="color:gray") &#xe60b;
font(color="#999").layui-word-aux Messenger 是 Facebook 提供给企业、商铺、消费者之间相互连接的即时通信工具,与 Facebook 粉丝页、Instagram 和网页聊天等多渠道快速发起对话,春松客服支持与 Messenger 集成,如有疑问,阅读文档中心,获得详细使用指南。
.col-lg-12
table.layui-table(lay-skin="line", style="table-layout: fixed; word-break: break-all")
thead
tr
th(style="width:100px") 名称
th 技能组
th Page ID
th(style="width:150px") 创建时间
th
| 状态
i.layui-icon(style="color:gray" data-toggle="tooltip" title="处于关闭状态,只对该渠道支持 Facebook OTN 功能;在开启状态,即支持客服功能也支持 Facebook OTN 功能") &#xe60b;
th Webhook
th(style="width:250px;white-space:nowrap;", nowrap="nowrap") 操作
tbody
for item in fbMessengers
tr
td(title="#{item.name}", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")= item.name
td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")= organs[item.organ].name
td(title="#{item.pageId}", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")= item.pageId
td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")= pugHelper.formatDate("yyyy-MM-dd HH:mm:ss", item.createtime)
td(style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")
.fbStatus.layui-unselect.layui-form-switch.checkStatus.lay-filter(style="margin-top: 0px;width: 50px;", data-id=item.id, class={
'layui-form-onswitch': item.status != 'disabled',
'layui-form-offswitch': item.status == 'disabled'
})
i.checkStatusI
td(style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")
button.layui-btn.layui-btn-small.webhook(type="button", data-pageid=item.pageId) 复制
td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")
a(href="/admin/messenger/setting.html?id=" + item.id, data-toggle="load", data-target="#me-config")
i.layui-icon 
| 设置
a(href="/admin/messenger/edit.html?id=" + item.id, style="margin-left:10px;", data-toggle="ajax", data-width="800", data-height="400", data-title="编辑 Messenger 渠道")
i.layui-icon 
| 编辑
a(href="https://www.facebook.com/" + item.pageId, style="margin-left:10px;", target="_blank")
i.layui-icon &#xe634;
| 粉丝页
a(href="/admin/messenger/delete.html?id=" + item.id, style="margin-left:10px;", data-toggle="tip", title="请确认是否删除?")
i.layui-icon(style="color:red;") ဆ
| 删除
style.
.fbStatus.layui-form-onswitch .checkStatusI {
left: 38px;
}
.fbStatus.layui-form-onswitch:before {
content: '开启';
color: #ffffff;
}
.fbStatus.layui-form-offswitch:before {
content: '关闭';
color: #aaaaaa;
padding-left: 18px;
}
script.
// 打开链接
function openExternalUrlWithBlankTarget(externalUrl) {
window.open(externalUrl, "_blank");
}
layui.use('layer', function () {
var layer = layui.layer;
var msg = '#{msg}'
if (msg == 'save_ok')
layer.msg('渠道添加成功', {icon: 1, time: 1000})
else if (msg == 'update_ok')
layer.msg('保存成功', {icon: 1, time: 1000})
else if (msg == 'save_no_PageId')
layer.msg('Page ID已存在', {icon: 2, time: 3000})
});
$('.fbStatus').click(function () {
var isOpen = $(this).hasClass('layui-form-onswitch')
var id = $(this).data('id');
var status = !isOpen ? 'enabled' : 'disabled';
var that = this;
$.post('setStatus.html', {id, status}).success(function (msg) {
if (msg == 'ok') {
if (isOpen) {
$(that).removeClass('layui-form-onswitch');
$(that).addClass('layui-form-offswitch');
} else {
$(that).removeClass('layui-form-offswitch');
$(that).addClass('layui-form-onswitch');
}
}
})
})
$('.webhook').each(function (i, n) {
var htmlOld = $(n).attr("data-pageId");
var webhook = window.location.protocol + "//" + window.location.host + "/messenger/webhook/" + htmlOld;
$(n).on('click', function () {
Clipboard.copy(webhook);
})
});
window.Clipboard = (function (window, document, navigator) {
var textArea,
copy;
// 判断是不是ios端
function isOS() {
return navigator.userAgent.match(/ipad|iphone/i);
}
//创建文本元素
function createTextArea(text) {
textArea = document.createElement('textArea');
textArea.value = text;
document.body.appendChild(textArea);
}
//选择内容
function selectText() {
var range,
selection;
if (isOS()) {
range = document.createRange();
range.selectNodeContents(textArea);
selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
textArea.setSelectionRange(0, 999999);
} else {
textArea.select();
}
}
//复制到剪贴板
function copyToClipboard() {
try {
if (document.execCommand("Copy")) {
layer.msg('webhook已复制到剪切板', {icon: 1, time: 1000, offset: 'rt'})
} else {
layer.msg('webhook复制失败请手动复制', {icon: 2, time: 1000, offset: 'rt'})
}
} catch (err) {
layer.msg('webhook复制失败请手动复制', {icon: 2, time: 1000, offset: 'rt'})
}
document.body.removeChild(textArea);
}
copy = function (text) {
createTextArea(text);
selectText();
copyToClipboard();
};
return {
copy: copy
};
})(window, document, navigator);

View File

@ -0,0 +1,133 @@
.row
.col-lg-12
.ukefu-customer-div.setting-wrapper
.box.default-box
.box-header
h1.site-h1 新建OTN
.row
.col-lg-12
.uk-layui-form(style="height: calc(100% + 10px);")
form.layui-form(action="/apps/messenger/otn/save.html", method="post")
input#preSubMessage(type="hidden", name="preSubMessage", value=otn.preSubMessage)
.layui-colla-item
h2.layui-colla-title 基本信息
.layui-colla-content.layui-show
.layui-form-item
.layui-inline
label.layui-form-label 名称:
.layui-input-inline
input.layui-input(type='text', name='name', required, lay-verify='required', autocomplete='off')
.layui-inline
font(color='red') *(必填项)
.layui-inline
label.layui-form-label(style='width:60px;line-height: 35px;') 渠道:
.layui-input-inline(style='width:218px;margin-right:0px;padding-top:9px;')
select(name='pageId', required, lay-verify='required')
option(value="") 请选择渠道...
if fbMessengers
for m in fbMessengers
option(value=m.pageId)= m.name
.layui-inline
font(color='red') *(必填项)
.layui-colla-item
h2.layui-colla-title 订阅邀请信息(2000字符)
.layui-colla-content.layui-show
blockquote.layui-elem-quote.layui-quote-nm
i.layui-icon(style="color:gray") &#xe60b;
font(color="#999").layui-word-aux 访客点击链接后发送订阅邀请信息给访客
.layui-form-item
.layui-inline(style="width: 380px;")
label.layui-form-label(style="float: none;text-align: left;width:200px")
| 订阅邀请前消息1
//font(color='red') *(必填项)
.layui-input-inline
#preSubMessage1.messageBox(name="preSubMessage1")
.layui-inline(style="width: 380px;")
label.layui-form-label(style="float: none;text-align: left;width:200px")
| 订阅邀请前消息2
//font(color='red') *(必填项)
.layui-input-inline
#preSubMessage2.messageBox(name="preSubMessage2")
.layui-form-item
.layui-inline(style="width: 320px;height: 209px;")
label.layui-form-label(style="float: none;text-align: left;width:200px")
| 订阅邀请
font(color='red') *(必填项,60字符
.layui-input-inline
textarea(name="subMessage", required="required", lay-verify="maxSubMessage", style="margin: 0px; width: 310px; height: 170px;resize:none;border: 1px solid #ccc")
.layui-colla-item
h2.layui-colla-title 订阅成功提醒(2000字符)
.layui-colla-content.layui-show
.layui-input-inline
.messageBox(name="successMessage")
.layui-colla-item
h2.layui-colla-title 推送信息(2000字符)
.layui-colla-content.layui-show
blockquote.layui-elem-quote.layui-quote-nm
i.layui-icon(style="color:gray") &#xe60b;
font(color="#999").layui-word-aux 推送时间到达时此内容通过OTN推送给访客。推送时间可以不设置后续手动推送。
.layui-form-item
.layui-inline(style="width: 380px;")
label.layui-form-label(style="float: none;text-align: left;width:200px") OTN内容
.layui-input-inline
.messageBox(name="otnMessage")
.layui-inline(style="width: 320px;height: 209px;")
label.layui-form-label(style="float: none;text-align: left;width:200px") 推送时间
.layui-input-inline
input#begin.layui-input.ukefu-input(name='sendtime', style="width:310px")
.layui-button-block(style="width: 700px;")
button.layui-btn(lay-submit="", lay-filter="formDemo") 提交
button.layui-btn.layui-btn-original(type="reset", onclick="location.reload()") 取消
script.
layui.use('form', function () {
var form = layui.form();
form.render(); //更新全部
form.verify({
maxSubMessage: function (value) {
if (value.length > 50) {
return '长度大于60请重新输入';
}
}
});
form.on('submit(formDemo)', function () {
});
});
layui.use('laydate', function () {
var laydate = layui.laydate;
document.getElementById('begin').onclick = function () {
var date = {
min: laydate.now(),
format: 'YYYY-MM-DD hh:mm:ss',
istoday: false,
istime: true
};
date.elem = this;
laydate(date);
}
})
layui.use('element', function () {
var element = layui.element();
});
$(".messageBox").otnContent();
function preSubMessageChange() {
var preSubMessage1 = JSON.parse($('#preSubMessage1 input').val() || '{}');
var preSubMessage2 = JSON.parse($('#preSubMessage2 input').val() || '{}');
$('#preSubMessage').val(JSON.stringify([preSubMessage1, preSubMessage2]))
}
$('#preSubMessage1').change(preSubMessageChange);
$('#preSubMessage2').change(preSubMessageChange);

View File

@ -0,0 +1,131 @@
.row
.col-lg-12
.ukefu-customer-div.setting-wrapper
.box.default-box
.box-header
h1.site-h1 编辑OTN
.row
.col-lg-12
.uk-layui-form(style="height: calc(100% + 10px);")
form.layui-form(action="/apps/messenger/otn/update.html", method="post")
input(type="hidden", name="pageId", value=pageId)
input(type="hidden", name="id", value=otn.id)
input#preSubMessage(type="hidden", name="preSubMessage", value=otn.preSubMessage)
.layui-colla-item
h2.layui-colla-title 基本信息
.layui-colla-content.layui-show
.layui-form-item
.layui-inline
label.layui-form-label 名称:
.layui-input-inline
input.layui-input(type='text', name='name', required, lay-verify='required', autocomplete='off', value=otn.name)
.layui-inline
font(color='red') *(必填项)
.layui-inline
label.layui-form-label(style='width:60px;line-height: 35px;') 渠道:
.layui-input-inline(style='width:218px;margin-right:0px;padding-top:9px;')
p(style="padding: 10px 0;")= otn.fbMessenger.name
.layui-colla-item
h2.layui-colla-title 订阅邀请信息(2000字符)
.layui-colla-content.layui-show
blockquote.layui-elem-quote.layui-quote-nm
i.layui-icon(style="color:gray") &#xe60b;
font(color="#999").layui-word-aux 访客点击链接后发送订阅邀请信息给访客
.layui-form-item
.layui-inline(style="width: 380px;")
label.layui-form-label(style="float: none;text-align: left;width:200px")
| 订阅邀请前消息1
//font(color='red') *(必填项)
.layui-input-inline
#preSubMessage1.messageBox(name="preSubMessage1")
.layui-inline(style="width: 380px;")
label.layui-form-label(style="float: none;text-align: left;width:200px")
| 订阅邀请前消息2
//font(color='red') *(必填项)
.layui-input-inline
#preSubMessage2.messageBox(name="preSubMessage2")
.layui-form-item
.layui-inline(style="width: 320px;height: 209px;")
label.layui-form-label(style="float: none;text-align: left;width:200px")
| 订阅邀请
font(color='red') *(必填项,65字符
.layui-input-inline
textarea(name="subMessage", required="required", lay-verify="maxSubMessage", style="margin: 0px; width: 310px; height: 170px;resize:none;border: 1px solid #ccc")= otn.subMessage
.layui-colla-item
h2.layui-colla-title 订阅成功提醒(2000字符)
.layui-colla-content.layui-show
.layui-input-inline
.messageBox(name="successMessage",value=otn.successMessage)
.layui-colla-item
h2.layui-colla-title 推送信息(2000字符)
.layui-colla-content.layui-show
blockquote.layui-elem-quote.layui-quote-nm
i.layui-icon(style="color:gray") &#xe60b;
font(color="#999").layui-word-aux 推送时间到达时此内容通过OTN推送给访客。推送时间可以不设置后续手动推送。
.layui-form-item
.layui-inline(style="width: 380px;")
label.layui-form-label(style="float: none;text-align: left;width:200px") OTN内容
.layui-input-inline
.messageBox(name="otnMessage",value=otn.otnMessage)
.layui-inline(style="width: 320px;height: 209px;")
label.layui-form-label(style="float: none;text-align: left;width:200px") 推送时间
.layui-input-inline
input#begin.layui-input.ukefu-input(name='sendtime', style="width:310px", value=pugHelper.formatDate("yyyy-MM-dd HH:mm:ss", otn.sendtime))
.layui-button-block(style="width: 700px;")
button.layui-btn(lay-submit="", lay-filter="formDemo") 提交
button.layui-btn.layui-btn-original(type="reset", onclick="location.reload()") 取消
script.
layui.use('form', function () {
var form = layui.form();
form.render(); //更新全部
form.verify({
maxSubMessage: function (value) {
if (value.length > 50) {
return '长度大于60请重新输入';
}
}
});
form.on('submit(formDemo)', function () {
});
});
layui.use('element', function () {
var element = layui.element();
});
layui.use('laydate', function () {
var laydate = layui.laydate;
document.getElementById('begin').onclick = function () {
var date = {
min: laydate.now(),
format: 'YYYY-MM-DD hh:mm:ss',
istoday: false,
istime: true
};
date.elem = this;
laydate(date);
}
})
var preSubMessage = JSON.parse($('#preSubMessage').val() || '[{},{}]');
$('#preSubMessage1').attr('value', JSON.stringify(preSubMessage[0]));
$('#preSubMessage2').attr('value', JSON.stringify(preSubMessage[1]));
$(".messageBox").otnContent();
function preSubMessageChange() {
var preSubMessage1 = JSON.parse($('#preSubMessage1 input').val() || '{}');
var preSubMessage2 = JSON.parse($('#preSubMessage2 input').val() || '{}');
$('#preSubMessage').val(JSON.stringify([preSubMessage1, preSubMessage2]))
}
$('#preSubMessage1').change(preSubMessageChange);
$('#preSubMessage2').change(preSubMessageChange);

View File

@ -0,0 +1,179 @@
extends /apps/include/layout.pug
block append head
script(src="/js/otnContent.js")
block content
.layui-side.layui-bg-black
.layui-side-scroll
include /apps/marketing/left.pug
#otn-edit-content.layui-body
.layui-side-scroll
.row
input#copyInput(type="hidden")
.col-lg-12
.ukefu-customer-div.setting-wrapper
.box.default-box
.box-header
h1.site-h1
| Messenger OTN 列表 &nbsp;
blockquote.layui-elem-quote.layui-quote-nm
i.layui-icon(style="color:gray") &#xe60b;
font(color="#999").layui-word-aux 请勿使用负载字段发送密码、用户凭证、可识别用户身份的信息(即,姓名或邮箱等可单独用于联系用户或识别其身份的信息)或其他敏感信息(如健康状况、财务、支付或持卡人数据,或根据适用法律定义为敏感信息的其他类别的信息
.row(style="padding:5px;")
h1(style="width: 100%;")
span(style="padding: 0 5px;") Messenger &nbsp;
select#queryPageId(name='queryPageId')
option(value) 请选择渠道...
for m in fbMessengers
option(value=m.pageId, selected=m.pageId == queryPageId)= m.name
span(style="float:right;")
button.layui-btn.layui-btn-small.green(href="/apps/messenger/otn/add.html", data-toggle="load", data-target="#otn-edit-content")
| 创建OTN
.row(style="padding:5px;")
.col-lg-12
table.layui-table(lay-skin="line", style="table-layout: fixed; word-break: break-all")
thead
tr
th 名称
th Messenger
th(style="width:100px") 点击
th(style="width:100px") 订阅
th(style="width:80px") 创建时间
th(style="width:80px") 发送时间
th(style="width:60px") 状态
th(style="width:60px") 分享链接
th(style="width:180px;white-space:nowrap;", nowrap="nowrap") 操作
tbody
- var state = {'create' :'新建','sending':'发送中','finish':'已发送'}
for item in otns.content
tr
td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")
| #{item.name}
td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")
| #{item.fbMessenger.name}
td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")
| #{item.melinkNum}
td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")
| #{item.subNum}
td
| #{pugHelper.formatDate("yyyy-MM-dd HH:mm:ss", item.createtime)}
td
| #{item.sendtime ? pugHelper.formatDate("yyyy-MM-dd HH:mm:ss", item.sendtime) : ''}
td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")
| #{state[item.status]}
td(style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")
button.layui-btn.layui-btn-small.webhook(type="button", data-id=item.id, data-page=item.pageId) 复制
td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")
if(item.status == 'create')
a(href="/apps/messenger/otn/send.html?id=" + item.id, style="margin-left:10px;", data-toggle="tip", title="请确认是否发送?")
i.layui-icon 
| 发送
a(href="/apps/messenger/otn/edit.html?id=" + item.id, data-toggle="load", data-target="#otn-edit-content")
i.layui-icon 
| 编辑
a(href="/apps/messenger/otn/delete.html?id=" + item.id, style="margin-left:10px;", data-toggle="tip", title="请确认是否删除?")
i.layui-icon(style="color:red;") ဆ
| 删除
.row(style='padding:5px;')
.col-lg-12#page(style='text-align:center;')
script.
layui.use(['laypage', 'layer'], function () {
var laypage = layui.laypage,
layer = layui.layer;
laypage({
cont: 'page',
pages: #{ otns.totalPages }, //总页数
curr: #{ otns.number + 1 },
groups: 5, //连续显示分页数
jump: function (data, first) {
if (!first) {
location.href = "/apps/messenger/otn/index.html?p=" + data.curr;
}
}
});
});
layui.use('layer', function () {
var layer = layui.layer;
var msg = '#{msg}'
if (msg == 'save_ok')
layer.msg('OTN创建成功', {icon: 1, time: 1000})
else if (msg == 'save_no_PageId')
layer.msg('Page ID已存在', {icon: 2, time: 3000})
else if (msg == 'send_ok')
layer.msg('发送成功', {icon: 1, time: 3000})
});
$(function () {
$('.webhook').each(function (i, n) {
var id = $(n).data("id");
var pageId = $(n).data("page");
var webhook = "https://m.me/" + pageId + "?ref=" + id;
$(n).on('click', function () {
Clipboard.copy(webhook);
})
});
$('#queryPageId').change(function () {
location.search = "queryPageId=" + $(this).val();
})
})
window.Clipboard = (function (window, document, navigator) {
var textArea,
copy;
// 判断是不是ios端
function isOS() {
return navigator.userAgent.match(/ipad|iphone/i);
}
//创建文本元素
function createTextArea(text) {
textArea = document.createElement('textArea');
textArea.value = text;
document.body.appendChild(textArea);
}
//选择内容
function selectText() {
var range,
selection;
if (isOS()) {
range = document.createRange();
range.selectNodeContents(textArea);
selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
textArea.setSelectionRange(0, 999999);
} else {
textArea.select();
}
}
//复制到剪贴板
function copyToClipboard() {
try {
if (document.execCommand("Copy")) {
layer.msg('me link 已复制到剪切板', {icon: 1, time: 1000, offset: 'rt'})
} else {
layer.msg('me link 复制失败请手动复制', {icon: 2, time: 1000, offset: 'rt'})
}
} catch (err) {
layer.msg('me link 复制失败请手动复制', {icon: 2, time: 1000, offset: 'rt'})
}
document.body.removeChild(textArea);
}
copy = function (text) {
createTextArea(text);
selectText();
copyToClipboard();
};
return {
copy: copy
};
})(window, document, navigator);

View File

@ -0,0 +1,86 @@
block content
include head
.layui-tab
.layui-tab-content
.layui-tab-item.layui-show
form.layui-form(method='post', action="/admin/messenger/setting/save.html")
input(type='hidden' name='id' value=fb.id)
input.layui-input(type='checkbox', name='status', lay-skin='switch', lay-text="开启|关闭")
.ukefu-customer-div.setting-wrapper
.box.default-box
.box-header
h3.box-title 机器人客服
.box-body(style="padding-top:5px;")
.row: .col-lg-8
.ukefu-webim-prop
.ukefu-webim-tl(style="clear:both;") 1、转人工按钮文本
.box-item
.row
.col-lg-8
p 默认显示信息:转人工
p(style="color:#888888;font-size:13px;margin-top:10px;") 访客在 Messenger 对话窗口,机器人客服转人工客服提示按钮文本
.col-lg-4
input.layui-input(type='text', name='transferManualService', lay-verify='required', value=(transferManualService ? transferManualService : "转人工"))
.ukefu-webim-prop
.ukefu-webim-tl(style="clear:both;") 2、推荐问题提示文本
.box-item
.row
.col-lg-8
p 默认显示信息:您是否想问以下问题
p(style="color:#888888;font-size:13px;margin-top:10px;") 机器人客服提示建议问题的文本
.col-lg-4
input.layui-input(type='text', name='suggestQuestion', lay-verify='required', value=(suggestQuestion ? suggestQuestion : "您是否想问以下问题"))
.ukefu-webim-prop
.ukefu-webim-tl(style="clear:both;") 3、询问是否有帮助文本
.box-item
.row
.col-lg-8
p 默认显示信息:以上答案是否对您有帮助
p(style="color:#888888;font-size:13px;margin-top:10px;") 机器人客服询问回答是否有帮助的文本
.col-lg-4
input.layui-input(type='text', name='evaluationAsk', lay-verify='required', value=(evaluationAsk ? evaluationAsk : "以上答案是否对您有帮助"))
.row: br
.row
.col-lg-8
p 有帮助按钮文本
p(style="color:#888888;font-size:13px;margin-top:10px;") 访客得到的上一条机器人回答有帮助
.col-lg-4
input.layui-input(type='text', name='evaluationYes', lay-verify='required', value=(evaluationYes ? evaluationYes : "是"))
.row: br
.row
.col-lg-8
p 有帮助后回复文本
p(style="color:#888888;font-size:13px;margin-top:10px;") 访客反馈反馈有帮助后机器人回复,设置为空则不回复
.col-lg-4
input.layui-input(type='text', name='evaluationYesReply', lay-verify='required', value=(evaluationYesReply ? evaluationYesReply : "感谢您的反馈,我们会做的更好!"))
.row: br
.row
.col-lg-8
p 无帮助按钮文本
p(style="color:#888888;font-size:13px;margin-top:10px;") 访客得到的上一条机器人回答没帮助
.col-lg-4
input.layui-input(type='text', name='evaluationNo', lay-verify='required', value=(evaluationNo ? evaluationNo : "否"))
.row: br
.row
.col-lg-8
p 无帮助后回复文本
p(style="color:#888888;font-size:13px;margin-top:10px;") 访客反馈反馈没帮助后机器人回复,设置为空则不回复
.col-lg-4
input.layui-input(type='text', name='evaluationNoReply', lay-verify='required', value=(evaluationNoReply ? evaluationNoReply : "感谢您的反馈,机器人在不断的学习!"))
.row
.col-lg-3
.col-lg-9
.layui-form-item
.layui-input-block
button.layui-btn(lay-submit, lay-filter='formDemo') 保存
button.layui-btn.layui-btn-original(type='reset', onclick="location.reload()") 取消
script.
layui.use('form', function () {
var form = layui.form;
});