/*
* Copyright 2015 Evgeny Dolganov (evgenij.dolganov@gmail.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 och.chat.service;
import static och.api.model.BaseBean.*;
import static och.api.model.PropKey.*;
import static och.api.model.chat.config.Key.*;
import static och.api.model.user.SecurityContext.*;
import static och.api.model.web.ReqInfo.*;
import static och.chat.service.SecurityService.*;
import static och.comp.ops.ChatOps.*;
import static och.util.Util.*;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import och.api.annotation.Secured;
import och.api.exception.chat.ChatAccountBlockedException;
import och.api.exception.chat.ChatAccountPausedException;
import och.api.exception.chat.NoChatAccountException;
import och.api.exception.chat.NoPreviousOperatorPositionException;
import och.api.exception.chat.OperatorPositionAlreadyExistsException;
import och.api.exception.client.AccountNotAddedToSessionException;
import och.api.exception.user.AccessDeniedException;
import och.api.model.chat.ChatLog;
import och.api.model.chat.ChatLogHist;
import och.api.model.chat.ChatOperator;
import och.api.model.chat.ChatUpdateData;
import och.api.model.chat.Feedback;
import och.api.model.chat.account.PrivilegeType;
import och.api.model.chat.config.Key;
import och.api.model.client.ClientInfo;
import och.api.model.client.ClientSession;
import och.api.model.user.User;
import och.api.model.user.UserRole;
import och.api.model.web.ReqInfo;
import och.chat.service.event.client.ClientSessionDesroyedEvent;
import och.chat.service.event.user.UserSessionDestroyedEvent;
import och.chat.service.model.UserSession;
import och.comp.chats.ChatsAcc;
import och.comp.chats.ChatsAccService;
import och.comp.chats.model.InitChatData;
import och.comp.mail.SendReq;
import och.util.concurrent.AsyncListener;
public class ChatsService extends BaseChatService {
SecurityService security;
ChatsAccService accs;
public ChatsService(ChatsAppContext c) {
super(c);
security = c.root.security;
accs = c.accs;
//reload by timer
long reloadDelay = c.props.getLongVal(chats_reloadDelay);
c.async.scheduleWithFixedDelay("reload chat accounts", ()->
accs.reloadAccs(),
reloadDelay, reloadDelay);
//events
c.events.addListener(ClientSessionDesroyedEvent.class, (event) -> closeChats(event.session));
c.events.addListener(UserSessionDestroyedEvent.class, (event) -> removeActiveOperators(event.session));
}
public void setAsyncListener(AsyncListener asyncListener){
accs.setAsyncListener(asyncListener);
}
public void checkChatAndInitClientSession(HttpServletRequest req, HttpServletResponse resp, String accId){
checkChatAndInitClientSession(req, resp, accId, null);
}
public void checkChatAndInitClientSession(HttpServletRequest req, HttpServletResponse resp, String accId, String oldChatId){
validateForText(accId, "accId");
//check chat's account
ChatsAcc acc = accs.findAcc(accId);
checkForBlockedOrPaused(accId);
//check online operators if need
ClientSession curSession = security.getClientSession(req);
if(curSession != null || oldChatId == null){
acc.checkHasActiveOperators();
}
//init or get session
boolean isNewSession = security.initClientSessionForAcc(req, accId);
if(!isNewSession) return;
//restore old chat
ClientSession clientSession = security.getClientSession(req);
ChatLog restored = accs.restoreOldChatIfNeed(accId, clientSession, oldChatId);
if(restored != null) log.info("["+accId+"] "+"CLIENT chat restored: "
+"chatId="+restored.id
+", msgCount="+restored.getMsgCount()
+", req="+getReqInfoStr());
}
public InitChatData addComment(String accId, ClientSession clientSession, String text){
ChatsAcc acc = checkClientAndFindChats(accId, clientSession);
checkForBlockedOrPaused(accId);
checkNewMsgToAdd(props, text);
InitChatData initResult = acc.initActiveChat(clientSession);
String chatId = initResult.id;
acc.addComment(clientSession, text);
log.info("["+accId+"] "+"CLIENT comment added: "
+"chatId="+chatId
+", size="+(isEmpty(text)? 0 : text.length())
+", req="+getReqInfoStr());
return initResult;
}
public void addFeedback(String accId, ClientInfo clientInfo, String text){
checkForBlocked(accId);
checkNewMsgToAdd(props, text);
ChatsAcc acc = accs.findAcc(accId);
//async add to file
accs.addFeedbackAsync(accId, clientInfo, text);
//emails
if( props.getBoolVal(chats_emailNotifications)
&& acc.getBoolVal(feedback_notifyOpsByEmail)) {
List<ChatOperator> ops = acc.getRegistredOperators();
List<String> emails = convert(ops, (op)-> op.email);
String accName = acc.getStrVal(Key.name);
sendFeedbackPosted(accId, accName, clientInfo, text, emails);
}
log.info("["+accId+"] "+"CLIENT feedback added: "
+"size="+(isEmpty(text)? 0 : text.length())
+", req="+getReqInfoStr());
}
public ChatLog getChatLog(String accId, ClientSession clientSession){
return getChatLog(accId, clientSession, null);
}
public ChatLog getChatLog(String accId, ClientSession clientSession, ChatUpdateData updateData){
ChatsAcc acc = checkClientAndFindChats(accId, clientSession);
ChatLog chatLog = acc.getActiveChat(clientSession, updateData);
return acc.fillOperators(chatLog);
}
public void closeChats(ClientSession session){
for (String accId : session.getAccIds()) {
ChatsAcc acc = accs.getAcc(accId);
if(acc == null) continue;
String chatId = acc.closeChat(session);
log.info("["+accId+"] "+"CLIENT chat closed: "
+"chatId="+chatId
+", req="+getReqInfoStr());
}
session.clearAccIds();
}
@Secured
public ChatLog getChatLogById(String accId, String chatId){
ChatsAcc acc = checkUserAccessAndFindChats(accId);
ChatLog chatLog = acc.getActiveChatById(chatId);
return acc.fillOperators(chatLog);
}
@Secured
public void createAcc(String accId){
checkAccessFor_ADMIN();
accs.createAcc(accId);
log.info("acc created: uid="+accId
+", req="+getReqInfoStr());
}
@Secured
public void removeAcc(String accId){
checkAccessFor_ADMIN();
accs.removeAcc(accId);
log.info("acc removed: uid="+accId
+", req="+getReqInfoStr());
}
@Secured
public Collection<String> getAccIds(){
checkAccessFor_ADMIN();
return accs.getAccIds();
}
@Secured
public void putOperator(String accId, ChatOperator operator){
ChatsAcc acc = checkUserAccessAndFindChats(accId);
acc.putOperator(operator);
log.info("["+accId+"] "+"OPERATOR added: opId="+operator.id
+", req="+getReqInfoStr());
}
@Secured
public void updateUserContact(long userId, String email) {
checkAccessFor_ADMIN();
List<ChatsAcc> allAccs = accs.getAllAccs();
for (ChatsAcc acc : allAccs) {
boolean updated = acc.updateOperatorContact(userId, email);
if(updated){
log.info("["+acc.getId()+"] "+"OPERATOR updated: opId="+userId
+", req="+getReqInfoStr());
}
}
}
@Secured
public void removeOperator(String accId, long opId){
ChatsAcc acc = checkUserAccessAndFindChats(accId);
acc.removeOperator(opId);
log.info("["+accId+"] "+"OPERATOR removed: opId="+opId
+", req="+getReqInfoStr());
}
@Secured
public void setActiveOperator(String accId, long operatorId){
ChatsAcc acc = checkUserAccessAndFindChats(accId);
boolean updated = acc.setActiveOperator(operatorId);
if(updated){
log.info("["+accId+"] "+"OPERATOR active: opId="+operatorId
+", req="+getReqInfoStr());
}
}
@Secured
public boolean isOperator(String accId, long opId){
ChatsAcc acc = checkUserAccessAndFindChats(accId);
return acc.isOperator(opId);
}
@Secured
public boolean isActiveOperator(String accId, long opId){
ChatsAcc acc = checkUserAccessAndFindChats(accId);
return acc.isActiveOperator(opId);
}
@Secured
public void removeActiveOperator(String accId, long opId){
ChatsAcc acc = checkUserAccessAndFindChats(accId);
boolean done = acc.removeActiveOperator(opId);
if(done) log.info("["+accId+"] "+"OPERATOR inactive: opId="+opId
+", req="+getReqInfoStr());
}
@Secured
public int getActiveOperatorsCount(String accId){
ChatsAcc acc = checkUserAccessAndFindChats(accId);
return acc.getActiveOperatorsCount();
}
@Secured
public ChatOperator addOperatorToChat(String accId, String chatId, long operatorId, int position) throws OperatorPositionAlreadyExistsException, NoPreviousOperatorPositionException {
ChatsAcc acc = checkUserAccessAndFindChats(accId);
checkForBlockedOrPaused(accId);
ChatOperator out = acc.addOperator(chatId, operatorId, position);
log.info("["+accId+"] "+"OPERATOR joined chat: "
+"chatId="+chatId
+", opId="+operatorId
+", index="+position
+", req="+getReqInfoStr());
return out;
}
@Secured
public ChatOperator getOperator(String accId, long operatorId){
ChatsAcc acc = checkUserAccessAndFindChats(accId);
ChatOperator op = acc.getOperator(operatorId);
return op == null? null : op.clone();
}
@Secured
public Collection<ChatLog> getActiveChatLogs(String accId, long operatorId){
ChatsAcc acc = checkUserAccessAndFindChats(accId);
return acc.getActiveChatsByOperator(operatorId);
}
@Secured
public Collection<ChatLog> getAllActiveChatLogs(String accId){
return getAllActiveChatLogs(accId, null);
}
@Secured
public Collection<ChatLog> getAllActiveChatLogs(String accId, Map<String, ChatUpdateData> updatesMap){
ChatsAcc acc = checkUserAccessAndFindChats(accId);
Collection<ChatLog> out = acc.getAllActiveChats(updatesMap);
return acc.fillOperators(out);
}
@Secured
public ChatLog getActiveChatLog(String accId, String chatId, ChatUpdateData updateData){
ChatsAcc acc = checkUserAccessAndFindChats(accId);
ChatLog log = acc.getActiveChat(chatId, updateData);
return acc.fillOperators(log);
}
@Secured
public void addComment(String accId, String chatId, long operatorId, String text){
ChatsAcc acc = checkUserAccessAndFindChats(accId);
checkForBlockedOrPaused(accId);
checkNewMsgToAdd(props, text);
acc.addComment(chatId, operatorId, text);
log.info("["+accId+"] "+"OPERATOR comment added: "
+"chatId="+chatId
+", opId="+operatorId
+", size="+(isEmpty(text)? 0 : text.length())
+", req="+getReqInfoStr());
}
@Secured
public List<ChatOperator> getRegistredOperators(String accId){
ChatsAcc acc = checkUserAccessAndFindChats(accId);
return acc.getRegistredOperators();
}
@Secured
public List<ChatLogHist> getHistory(String accId, Date date){
ChatsAcc acc = checkUserAccessAndFindChats(accId);
List<ChatLogHist> out = accs.getHistory(accId, date);
acc.fillOperators(out);
return out;
}
@Secured
public List<Feedback> getFeedbacks(String accId, Date date){
checkUserAccessAndFindChats(accId);
List<Feedback> out = accs.getFeedbacks(accId, date);
return out;
}
@Secured
public void setAccBlocked(String accId, boolean val){
checkAccessFor_ADMIN();
boolean done = accs.setBlocked(accId, val);
if(done) log.info("["+accId+"] "+(val? "blocked" : "unblocked")+", req="+getReqInfoStr());
}
public boolean isAccBlocked(String accId){
return accs.isBlocked(accId);
}
@Secured
public void setAccPaused(String accId, boolean val){
checkAccessFor_ADMIN();
boolean done = accs.setPaused(accId, val);
if(done) log.info("["+accId+"] "+(val? "paused" : "unpaused")+", req="+getReqInfoStr());
}
public boolean isAccPaused(String accId){
return accs.isPaused(accId);
}
public boolean isAccExists(String accId){
ChatsAcc acc = accs.getAcc(accId);
return acc != null;
}
@Secured
public void putAccConfig(String accId, Key key, String val) {
checkAccessFor_ADMIN();
ChatsAcc acc = accs.getAcc(accId);
if(acc == null) return;
acc.putConfig(key, val);
log.info("["+accId+"] "+"put config: "
+"key="+key
+", val="+val
+", req="+getReqInfoStr());
}
@Secured
public String getAccConfig(String accId, Key key){
checkAccessFor_ADMIN();
ChatsAcc acc = accs.getAcc(accId);
if(acc == null) return null;
return acc.getStrVal(key);
}
private ChatsAcc checkClientAndFindChats(String accId, ClientSession clientSession) throws NoChatAccountException {
if( ! clientSession.containsAccId(accId)) {
throw new AccountNotAddedToSessionException(accId);
}
return accs.findAcc(accId);
}
private ChatsAcc checkUserAccessAndFindChats(String accId, PrivilegeType... validTypes) throws AccessDeniedException, NoChatAccountException {
checkAccessForUser(accId, validTypes);
return accs.findAcc(accId);
}
private void checkForBlockedOrPaused(String accId){
checkForBlocked(accId);
if(accs.isPaused(accId)) throw new ChatAccountPausedException(accId);
}
private void checkForBlocked(String accId){
if(accs.isBlocked(accId)) throw new ChatAccountBlockedException(accId);
}
private void removeActiveOperators(UserSession session){
pushToSecurityContext_SYSTEM_USER();
try {
long userId = session.userId;
for (String accId : accs.getAccIds()) {
removeActiveOperator(accId, userId);
}
} finally {
popUserFromSecurityContext();
}
}
private void sendFeedbackPosted(String accId, String accName, ClientInfo clientInfo, String text, List<String> emails) {
if(isEmpty(emails)) return;
ReqInfo reqInfo = getReqInfo();
try {
HashMap<String, String> params = new HashMap<>();
params.put("accName", hasText(accName)? accName : accId);
params.put("userName", clientInfo.name == null? "" : clientInfo.name);
params.put("userEmail", clientInfo.email == null? "" : clientInfo.email);
params.put("reqInfo", reqInfo == null? "" : reqInfo.getFinalRef());
params.put("text", text == null? "" : text);
String replayTo = clientInfo.email;
String subject = c.templates.fromTemplate("feedback-subject.ftl", params);
String html = c.templates.fromTemplate("feedback-text.ftl", params);
SendReq req = new SendReq(emails, subject, html);
req.replyTo(replayTo);
c.mails.sendAsync(req);
} catch (Exception e) {
log.error("can't send email", e);
}
}
private static void checkAccessForUser(String accId, PrivilegeType... validTypes){
boolean hasAccess = hasAccessForUser(accId, validTypes);
if( ! hasAccess){
throw new AccessDeniedException();
}
}
private static boolean hasAccessForUser(String accId, PrivilegeType... validTypes){
if(hasAccessFor(UserRole.ADMIN)) return true;
User user = findUserFromSecurityContext();
Map<String, Set<PrivilegeType>> privilegesByAcc = user.getParam(PRIVILEGES_PARAM);
Set<PrivilegeType> set = privilegesByAcc.get(accId);
if(isEmpty(validTypes)){
return ! isEmpty(set);
}
for(PrivilegeType valid : validTypes){
if(set.contains(valid)){
return true;
}
}
return false;
}
}