/* * 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.front.service; import static java.math.BigDecimal.*; import static java.util.Collections.*; import static och.api.model.BaseBean.*; import static och.api.model.PropKey.*; import static och.api.model.RemoteChats.*; import static och.api.model.RemoteFront.*; import static och.api.model.billing.PaymentType.*; import static och.api.model.chat.account.ChatAccountPrivileges.*; import static och.api.model.chat.account.PrivilegeType.*; import static och.api.model.tariff.Tariff.*; import static och.api.model.tariff.TariffMath.*; import static och.api.model.user.SecurityContext.*; import static och.api.model.web.ReqInfo.*; import static och.api.remote.front.ReloadChatsModelType.*; import static och.comp.db.main.table.MainTables.*; import static och.comp.ops.BillingOps.*; import static och.comp.ops.ChatOps.*; import static och.comp.ops.RemoteOps.*; import static och.comp.ops.SecurityOps.*; import static och.comp.web.JsonOps.*; import static och.front.service.chat.AccConfigOps.*; import static och.util.DateUtil.*; import static och.util.ExceptionUtil.*; import static och.util.StringUtil.*; import static och.util.Util.*; import static och.util.concurrent.DoneFuture.*; import static och.util.servlet.WebUtil.*; import static och.util.sql.SingleTx.*; import java.math.BigDecimal; import java.net.URL; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.Set; import java.util.concurrent.Future; import javax.servlet.http.HttpServletRequest; import och.api.annotation.Secured; import och.api.exception.ExpectedException; import och.api.exception.InvalidInputException; import och.api.exception.chat.AccountsLimitException; import och.api.exception.chat.AddUserReqAlreadyExistsException; import och.api.exception.chat.ChatAccountBlockedException; import och.api.exception.chat.ChatAccountNotPausedException; import och.api.exception.chat.ChatAccountPausedException; import och.api.exception.chat.HostBlockedException; import och.api.exception.chat.NoAvailableServerException; import och.api.exception.chat.NoChatAccountException; import och.api.exception.chat.UserAlreadyInAccountException; import och.api.exception.tariff.ChangeTariffLimitException; import och.api.exception.tariff.InvalidTariffException; import och.api.exception.tariff.NotPublicTariffException; import och.api.exception.tariff.OperatorsLimitException; import och.api.exception.tariff.TariffNotFoundException; import och.api.exception.tariff.UpdateTariffOperatorsLimitException; import och.api.exception.user.AccessDeniedException; import och.api.exception.user.UserNotFoundException; import och.api.model.billing.UserBalance; import och.api.model.chat.account.ChatAccount; import och.api.model.chat.account.ChatAccountAddReq; import och.api.model.chat.account.ChatAccountPrivileges; import och.api.model.chat.account.PauseAccResp; import och.api.model.chat.account.PrivilegeType; import och.api.model.chat.config.Key; import och.api.model.chat.host.ClientHost; import och.api.model.server.ServerRow; import och.api.model.tariff.Tariff; import och.api.model.user.UpdateUserReq; import och.api.model.user.User; import och.api.model.user.UserExt; import och.api.model.user.UserRole; import och.api.remote.chats.CreateAccountReq; import och.api.remote.chats.InitUserTokenReq; import och.api.remote.chats.PutAccConfigReq; import och.api.remote.chats.PutOperatorReq; import och.api.remote.chats.RemoveOperatorReq; import och.api.remote.chats.RemoveUserSessionReq; import och.api.remote.chats.SetAccsPausedReq; import och.api.remote.chats.UpdateUserContactReq; import och.api.remote.chats.UpdateUserSessionsReq; import och.api.remote.front.ReloadChatsModelReq; import och.api.remote.front.ReloadChatsModelType; import och.comp.cache.client.CacheClient; import och.comp.db.base.universal.UpdateRows; import och.comp.db.main.table._f.IsFull; import och.comp.db.main.table._f.TariffChangedInDay; import och.comp.db.main.table._f.TariffId; import och.comp.db.main.table._f.TariffLastPay; import och.comp.db.main.table._f.TariffPrevId; import och.comp.db.main.table._f.TariffStart; import och.comp.db.main.table.billing.SelectUserBalanceById; import och.comp.db.main.table.chat.CreateChatAccount; import och.comp.db.main.table.chat.GetChatAccount; import och.comp.db.main.table.chat.UpdateChatAccountByUid; import och.comp.db.main.table.chat.addreqs.CreateChatAccountAddReqs; import och.comp.db.main.table.chat.addreqs.DeleteChatAccountAddReq; import och.comp.db.main.table.chat.addreqs.GetAllChatAccountAddReqsByAcc; import och.comp.db.main.table.chat.addreqs.GetAllChatAccountAddReqsByUser; import och.comp.db.main.table.chat.host.CreateClientHost; import och.comp.db.main.table.chat.host.CreateClientHostAcc; import och.comp.db.main.table.chat.host.CreateClientHostAccOwner; import och.comp.db.main.table.chat.host.GetClientHost; import och.comp.db.main.table.chat.privilege.CreateChatAccountPrivileges; import och.comp.db.main.table.chat.privilege.DeleteChatAccountPrivilege; import och.comp.db.main.table.chat.privilege.UpdateChatAccountPrivileges; import och.comp.db.main.table.chat.privilege.UpdateUserAccNickname; import och.comp.db.main.table.server.CreateServer; import och.comp.db.main.table.server.GetServerById; import och.comp.db.main.table.server.UpdateServerById; import och.comp.db.main.table.tariff.CreateTariff; import och.comp.db.main.table.user.SelectUserById; import och.front.service.chat.HostsStat; import och.front.service.chat.ReloadOps; import och.front.service.event.admin.UpdateModelsEvent; import och.front.service.event.chat.ChatCreatedEvent; import och.front.service.event.user.UserSessionDesroyedEvent; import och.front.service.event.user.UserUpdateTxEvent; import och.front.service.model.ChatsModel; import och.front.service.model.UserAccInfo; import och.front.service.model.UserSession; import och.util.model.CallableVoid; import och.util.model.Pair; import och.util.servlet.WebUtil; import och.util.sql.ConcurrentUpdateSqlException; public class ChatService extends BaseFrontService { public static final String ACC_SESSION_TOKEN = "chat.accSessionToken"; public static final String ACC_INITED_URLS = "chat.accInitedUrls"; private volatile ChatsModel m; private volatile HostsStat hostsStat; SecurityService security; BillingService billing; Random r = new Random(); CacheClient cache; ReloadOps reloadOps; public ChatService(FrontAppContext c) { super(c); } @Override public void init() throws Exception { security = c.root.security; billing = c.root.billing; cache = c.cache; reloadOps = new ReloadOps(c); reloadModel(); //events c.events.addListener(UserSessionDesroyedEvent.class, (event)-> sendRemoveUserSessionsAsync(event.userSession)); c.events.addListener(UpdateModelsEvent.class, (event) -> { reloadModel(); sendUpdateOtherFrontsSignalAsync(FULL_MODEL_UPDATED); }); c.events.addListener(UserUpdateTxEvent.class, (event)-> updateRemoteContactsOnUserUpdateTx(event)); //timer ops if(props.getBoolVal(chats_hosts_stat_use)){ hostsStat = new HostsStat(); c.async.tryScheduleWithFixedDelay("flush-hosts-statistics", ()-> saveClientsHostsStatImpl(), props.getLongVal(chats_hosts_stat_FlushDelay), props.getLongVal(chats_hosts_stat_FlushDelta)); } } public void reloadModel() throws Exception{ reloadModel(null); } public synchronized void reloadModel(ReloadChatsModelReq req) throws Exception{ ReloadChatsModelType type = req != null? req.type() : FULL_MODEL_UPDATED; if(type == FULL_MODEL_UPDATED){ this.m = reloadOps.loadFullModel(); } else if(type == SERVER_CREATED){ reloadOps.reloadServer(m, req.getLongParam1()); } else if(type == SERVER_UPDATED){ reloadOps.reloadServer(m, req.getLongParam1()); } else if(type == ACC_CREATED){ reloadOps.reloadNewAcc(m, req.getLongParam1(), req.param2); } else if(type == ACC_UPDATED){ reloadOps.reloadAcc(m, req.param1); } else if(type == TARIFF_CREATED){ reloadOps.reloadTariff(m, req.getLongParam1()); } else if(type == USER_PRIVS_UPDATED){ reloadOps.reloadUserPrivs(m, req.getLongParam1(), req.getLongParam2()); } log.info("model reloaded: type="+type +", req="+getReqInfoStr()); } public ServerRow getServerByAcc(String accUid) { return m.getServerByAcc(accUid); } public Set<PrivilegeType> getAccPrivilegesForUser(long chatId, long userId){ return m.getPrivilegesForAcc(userId, chatId).privs; } public Set<PrivilegeType> getAccPrivilegesForUser(String accUid, long userId){ return m.getPrivilegesForAcc(userId, accUid).privs; } public Map<String, Set<PrivilegeType>> getAllAccountsPrivilegesForUser(long userId){ return m.getPrivilegesForAccs(userId); } /** * Список аккаунтов с серверами в которых юзер - оператор или модерадор или админ. * Root роль не дает права получить все аккаунты. */ public List<ChatAccount> getAccsForOperator(long userId) { List<ChatAccount> out = m.getAccountsForOperator(userId); for (ChatAccount acc : out) { acc.blocked = isAccBlockedFromCache(cache, acc.uid); } return out; } public List<String> getOwnerAccIds(long userId){ return new ArrayList<>(m.getAccountsUidsFor(userId, PrivilegeType.CHAT_OWNER)); } public boolean checkAllAccExists(long serverId, List<String> accs) { if(isEmpty(accs)) return false; for (String accUid : accs) { if(isEmpty(accUid)) return false; ChatAccount acc = m.getAccount(accUid, false); if(acc == null) return false; if(acc.serverId != serverId) return false; } return true; } /** * Послать на сервера чатов токены юзера для их автозаведения сессии при доступе из браузера */ public String initUserTokenInAccServers(HttpServletRequest req) throws Exception{ return initUserTokenInAccServers(req, false); } public String initUserTokenInAccServers(HttpServletRequest req, boolean full) throws Exception{ //no session User user = security.getUserFromSession(req); if(user == null) return null; long userId = user.id; Set<String> reqUrls = getChatServerUrlReqsFor(userId, URL_USER_INIT_TOKEN); if(isEmpty(reqUrls)) return null; //get or create token String token = security.getUserSessionAttr(req, ACC_SESSION_TOKEN); if(token == null){ token = randomSimpleId(); } //filter done urls if need Map<String, Void> doneUrls = security.getUserSessionAttr(req, ACC_INITED_URLS); if(doneUrls == null){ doneUrls = new HashMap<>(); security.setUserSessionAttr(req, ACC_INITED_URLS, doneUrls); } if( ! full){ for(String doneUrl : doneUrls.keySet()){ reqUrls.remove(doneUrl); } } if(isEmpty(reqUrls)) return token; String clientIp = getClientIp(req); String userAgent = getUserAgent(req); //remote if(isUseRemote(props)){ //cur privilages Map<String, Set<PrivilegeType>> privilegesByAccount = m.getPrivilegesForAccs(userId); InitUserTokenReq remoteReq = new InitUserTokenReq(token, userId, clientIp, userAgent, privilegesByAccount); Map<String, Void> results = postEncryptedJsonToAny(props, reqUrls, remoteReq); doneUrls.putAll(results); } security.setUserSessionAttr(req, ACC_SESSION_TOKEN, token); log.info("sended Create session tokens: userId="+user.id +", login="+user.login +", reqUrls="+reqUrls +", req="+getReqInfoStr()); return token; } private void sendRemoveUserSessionsAsync(UserSession userSession) { String token = (String)userSession.attrs.get(ACC_SESSION_TOKEN); if(token == null) return; User user = userSession.user; Set<String> reqUrls = getChatServerUrlReqsFor(user.id, URL_USER_REMOVE_SESSION); if(isEmpty(reqUrls)) return; //remote if(isUseRemote(props)){ c.async.invoke(()-> postEncryptedJsonToAny(props, reqUrls, new RemoveUserSessionReq(token)) ); } log.info("sended Remove session tokens: userId="+user.id +", login="+user.login +", reqUrls="+reqUrls +", req="+getReqInfoStr()); } private Set<String> getChatServerUrlReqsFor(long userId, String req){ Collection<ChatAccount> accs = m.getAccountsForOperator(userId); if(isEmpty(accs)) return null; HashSet<String> reqUrls = new HashSet<>(); for(ChatAccount acc : accs){ reqUrls.add(acc.server.createUrl(req)); } return reqUrls; } @Secured public long createServer(String httpUrl, String httpsUrl) throws Exception{ checkAccessFor_ADMIN(); //update db long id = universal.nextSeqFor(servers); universal.update(new CreateServer(id, httpUrl, httpsUrl)); ServerRow server = new ServerRow(id, httpUrl, httpsUrl); //update model m.putServer(server); //update other fronts servers sendUpdateOtherFrontsSignalAsync(SERVER_CREATED, id); log.info("server created: id="+id +", httpUrl="+httpUrl +", httpsUrl="+httpsUrl +", req="+getReqInfoStr()); return id; } @Secured public void setServerFull(long serverId, boolean val) throws Exception { checkAccessFor_ADMIN(); //read from model ServerRow server = m.getServer(serverId); if(server == null) return; if(server.isFull == val) return; //update db universal.update(new UpdateServerById(serverId, new IsFull(val))); //update model m.updateServerFull(serverId, val); //update other fronts servers sendUpdateOtherFrontsSignalAsync(SERVER_UPDATED, serverId); log.info("server changed full value: id="+server.id +", val="+val +", req="+getReqInfoStr()); } @Secured public long createTariff(BigDecimal price, boolean isPublic, int maxOperators) throws Exception { checkAccessFor_ADMIN(); return createTariff(price, isPublic, maxOperators, null); } @Secured public long createTariff(BigDecimal price, boolean isPublic, int maxOperators, Long idPreset) throws Exception { checkAccessFor_ADMIN(); //update db long id = idPreset != null? idPreset : universal.nextSeqFor(tariffs); Tariff tariff = new Tariff(id, price, isPublic, maxOperators); universal.update(new CreateTariff(tariff)); //update model m.putTariff(tariff); //update other fronts servers sendUpdateOtherFrontsSignalAsync(TARIFF_CREATED, id); log.info("tariff created: id="+id +", req="+getReqInfoStr()); return id; } public List<Tariff> getPublicTariffs(){ return m.getPublicTariffs(); } @Secured public String createAccByUser(String name) throws NoAvailableServerException, UserNotFoundException, Exception{ return createAccByUser(name, null); } @Secured public String createAccByUser(String name, Long tariffId) throws NoAvailableServerException, UserNotFoundException, Exception{ if(props.getBoolVal(toolMode)){ checkAccessFor_ADMIN(); } long userId = findUserIdFromSecurityContext(); checkUserAccsForBlocked(); Set<String> existsAccs = m.getAccountsUidsFor(userId, CHAT_OWNER); long maxAccs = props.getVal(chats_maxAccsForUser+"-"+userId, 0); if(maxAccs == 0) maxAccs = props.getIntVal(chats_maxAccsForUser); if(existsAccs.size() >= maxAccs) throw new AccountsLimitException(); if(tariffId == null) tariffId = props.getLongVal(tariffs_default_tariff); List<Long> serversIds = m.getNotFullServersId(); int size = serversIds.size(); if(size == 0) throw new NoAvailableServerException(); //check tariff Tariff tariff = m.getTariff(tariffId); if(tariff == null) throw new TariffNotFoundException(tariffId); if( ! tariff.isPublic) throw new InvalidTariffException(tariffId); //random server Long serverId = serversIds.get(r.nextInt(size)); String uid = randomUUID(); pushToSecurityContext_SYSTEM_USER(); try { createAcc(serverId, uid, userId, name, tariffId); return uid; }finally { popUserFromSecurityContext(); } } @Secured public ChatAccount getAccByUid(String accUid, boolean withServer){ checkAccessFor_ADMIN(); return m.getAccount(accUid, withServer); } @Secured public long createAcc( long serverId, String uid, long ownerId, String name, long tariffId) throws UserNotFoundException, Exception{ return createAcc(serverId, uid, ownerId, name, tariffId, true); } @Secured public long createAcc( long serverId, String uid, long ownerId, String name, long tariffId, boolean adminNotify) throws UserNotFoundException, Exception{ checkAccessFor_ADMIN(); if(hasText(name) && name.length() > ChatAccount.MAX_NAME_SIZE){ throw new InvalidInputException("too long name: "+name); } //check tariff Tariff tariff = m.getTariff(tariffId); if(tariff == null) throw new TariffNotFoundException(tariffId); Date created = new Date(); long accId = -1; ServerRow server = null; ChatAccount acc = null; ChatAccountPrivileges ownerPriv = null; //update db setSingleTxMode(); try { server = universal.selectOne(new GetServerById(serverId)); if(server == null) return -1; accId = universal.nextSeqFor(chat_accounts); acc = new ChatAccount(accId, serverId, uid, name, created, tariffId); universal.update(new CreateChatAccount(acc)); Set<PrivilegeType> set = singleton(CHAT_OWNER); universal.update(new CreateChatAccountPrivileges(accId, ownerId, set)); ownerPriv = new ChatAccountPrivileges(ownerId, accId, set); //remote if(isUseRemote(props)){ postEncryptedJson(props, server.createUrl(URL_CHAT_CREATE_ACC), new CreateAccountReq(uid)); } }catch (Exception e) { rollbackSingleTx(); String msg = e.getMessage(); if(hasText(msg) && msg.toLowerCase().contains("userid")){ throw new UserNotFoundException(ownerId); } throw e; } finally { closeSingleTx(); } //update model m.putData(server, acc, ownerPriv); //send remote updates sendUpdateChatServerSessions(server, ownerId); sendUpdateOtherFrontsSignalAsync(ACC_CREATED, ownerId, uid); //post event c.events.tryFireEvent(new ChatCreatedEvent(acc, ownerId)); String logMsg = "acc created: " + "uid="+acc.uid +", name="+acc.name +", tariffId="+tariff.id +", ownerId="+ownerId +", serverId="+server.id +", serverAccsCount="+m.getServerAccountsCount(server.id) +", serverUrl="+server.httpUrl +", req="+getReqInfoStr(); if(adminNotify && props.getBoolVal(admin_emailNotify_AccCreated)){ c.mails.sendAsyncInfoData("acc created", logMsg); } log.info(logMsg); return acc.id; } @Secured public void putAccConfigByUser(String uid, Key key, Object val)throws Exception { checkPrivilegesForAcc_Owner(uid); pushToSecurityContext_SYSTEM_USER(); try { putAccConfig(uid, key, val); }finally { popUserFromSecurityContext(); } } @Secured public void putAccConfig(String uid, Key key, Object val) throws Exception { checkAccessFor_ADMIN(); ChatAccount acc = m.getAccount(uid, true); if(acc == null) return; Pair<UpdateRows, PutAccConfigReq> data = validateReqAndGetUpdates(uid, key, val); if(data == null) return; //update db and remote acc doInSingleTxMode(()->{ universal.update(data.first); if(isUseRemote(props)){ PutAccConfigReq req = data.second; postEncryptedJson(props, acc.server.createUrl(URL_CHAT_PUT_ACC_CONFIG), req); } }); //update model m.putAccConfig(uid, key, val); //send remote updates sendUpdateOtherFrontsSignalAsync(ACC_UPDATED, uid); log.info("acc config updated: uid="+acc.uid +", key="+key +", val="+val +", req="+getReqInfoStr()); } @Secured public PauseAccResp pauseAccByUser(String accUid) throws Exception{ checkPrivilegesForAcc_Owner(accUid); checkUserAccsForBlocked(); pushToSecurityContext_SYSTEM_USER(); try { return pauseAcc(accUid, null); }finally { popUserFromSecurityContext(); } } @Secured public PauseAccResp pauseAcc(String accUid, UpdateTariffOps reqOps) throws Exception{ checkAccessFor_ADMIN(); ChatAccount acc = m.findAccount(accUid, true); if(reqOps == null){ boolean canUseNotPublic = true; boolean checkMaxChangedInDay = true; reqOps = new UpdateTariffOps(canUseNotPublic, checkMaxChangedInDay); } //extra tx logic reqOps.beforeTxCommitListener = ()-> { if( ! isUseRemote(props)) return; postEncryptedJson(props, acc.server.createUrl(URL_CHAT_PAUSED), new SetAccsPausedReq(accUid, true)); }; long newTariffId = PAUSE_TARIFF_ID; BigDecimal cost = updateAccTariff(accUid, newTariffId, reqOps); PauseAccResp out = new PauseAccResp(newTariffId, cost); log.info("acc paused: uid="+acc.uid +", req="+getReqInfoStr()); return out; } @Secured public PauseAccResp unpauseAccByUser(String accUid) throws Exception { checkPrivilegesForAcc_Owner(accUid); checkUserAccsForBlocked(); ChatAccount acc = m.findAccount(accUid, false); if(PAUSE_TARIFF_ID != acc.tariffId) throw new ChatAccountNotPausedException(); Long tariffPrevId = acc.tariffPrevId; if(tariffPrevId == null){ log.error("invalid state: tariffPrevId is null for paused acc: "+accUid); //try to select List<Tariff> publicTariffs = m.getPublicTariffs(); if(isEmpty(publicTariffs)) throw new IllegalStateException("no public tariffs to select"); tariffPrevId = publicTariffs.get(0).id; } pushToSecurityContext_SYSTEM_USER(); try { return unpauseAcc(accUid, tariffPrevId, null); }finally { popUserFromSecurityContext(); } } @Secured public PauseAccResp unpauseAcc(String accUid, long tariffId, UpdateTariffOps reqOps) throws Exception { checkAccessFor_ADMIN(); if(tariffId == PAUSE_TARIFF_ID) throw new InvalidInputException("tariff must be not 'paused' for acc: "+accUid); ChatAccount acc = m.findAccount(accUid, true); if(reqOps == null){ boolean canUseNotPublic = true; boolean checkMaxChangedInDay = false; reqOps = new UpdateTariffOps(canUseNotPublic, checkMaxChangedInDay); } //extra tx logic reqOps.beforeTxCommitListener = ()-> { if( ! isUseRemote(props)) return; postEncryptedJson(props, acc.server.createUrl(URL_CHAT_PAUSED), new SetAccsPausedReq(accUid, false)); }; BigDecimal cost = updateAccTariff(accUid, tariffId, reqOps); PauseAccResp out = new PauseAccResp(tariffId, cost); log.info("acc unpaused: uid="+acc.uid +", tariffId="+tariffId +", req="+getReqInfoStr()); return out; } @Secured public BigDecimal updateAccTariffByUser(String accUid, long newTariffId) throws Exception{ checkPrivilegesForAcc_Owner(accUid); checkUserAccsForBlocked(); checkAccForPaused(accUid); if(newTariffId == PAUSE_TARIFF_ID) throw new InvalidInputException("use 'pauseAcc' method for this system tariff"); pushToSecurityContext_SYSTEM_USER(); try { boolean canUseNotPublic = false; boolean checkMaxChangedInDay = true; return updateAccTariff(accUid, newTariffId, new UpdateTariffOps(canUseNotPublic, checkMaxChangedInDay)); }finally { popUserFromSecurityContext(); } } @Secured public BigDecimal updateAccTariff(String accUid, long newTariffId, boolean canUseNotPublic) throws Exception { checkAccessFor_ADMIN(); boolean checkMaxChangedInDay = false; return updateAccTariff(accUid, newTariffId, new UpdateTariffOps(canUseNotPublic, checkMaxChangedInDay)); } @Secured public BigDecimal updateAccTariff( String accUid, long newTariffId, UpdateTariffOps reqOps) throws ConcurrentUpdateSqlException, Exception { checkAccessFor_ADMIN(); UpdateTariffOps ops = reqOps != null? reqOps : new UpdateTariffOps(); //берем из БД ChatAccount acc = universal.selectOne(new GetChatAccount(accUid)); if(acc == null) throw new NoChatAccountException(); if(acc.tariffId == newTariffId) return ZERO; //old and new tariffs Tariff oldTariff = m.findTariff(acc.tariffId); Tariff newTariff = m.findTariff(newTariffId); if( ! newTariff.isPublic && ! ops.canUseNotPublic) throw new NotPublicTariffException(newTariffId); //check limitations int maxOperators = newTariff.maxOperators; if(maxOperators > 0){ Map<Long, UserAccInfo> curOperators = m.getAccOperators(accUid); if( ! props.getBoolVal(toolMode) && curOperators.size() > maxOperators) throw new UpdateTariffOperatorsLimitException(); } Date now = ops.nowPreset == null? new Date() : ops.nowPreset; Date tariffStart = ops.tariffStartPreset == null? acc.tariffStart : ops.tariffStartPreset; Date tariffLastPay = ops.tariffLastPayPreset == null? acc.tariffLastPay : ops.tariffLastPayPreset; //changes in same day int changedInDay; if( ! isSameDay(tariffStart, now)) changedInDay = 1; else { if(ops.checkMaxChangedInDay){ changedInDay = acc.tariffChangedInDay + 1; Integer maxChanges = props.getIntVal(tariffs_maxChangedInDay); if( ! props.getBoolVal(toolMode) && maxChanges > 0 && changedInDay > maxChanges) throw new ChangeTariffLimitException(); } else { changedInDay = acc.tariffChangedInDay; } } //calculate bill long ownerId = m.findAccOwner(accUid); BigDecimal price = ops.pricePreset == null? oldTariff.price : ops.pricePreset; BigDecimal minPrice = props.getBigDecimalVal(tariffs_minChangeTariffBill); BigDecimal amount = calcForPeriod(price, tariffLastPay, now, minPrice); boolean hasBill = ZERO.compareTo(amount) != 0; Date prevLastPay = acc.tariffLastPay; //update db doInSingleTxMode(()->{ //extra tx logic if(ops.beforeTxBeginListener != null){ ops.beforeTxBeginListener.call(); } //bill if(hasBill) { String desc = "accId="+acc.id+", oldTariff="+oldTariff.id+", newTariff="+newTariff.id; billing.payBill(ownerId, amount, now, TARIFF_CHANGE_BIll, desc, false); } //set new tariff int result = universal.updateOne(new UpdateChatAccountByUid( accUid, prevLastPay, new TariffId(newTariff.id), new TariffStart(now), new TariffLastPay(now), new TariffChangedInDay(changedInDay), new TariffPrevId(oldTariff.id))); //concurrent check if(result == 0) throw new ConcurrentUpdateSqlException("UpdateChatAccountByUid: uid="+accUid); //extra tx logic if(ops.beforeTxCommitListener != null){ ops.beforeTxCommitListener.call(); } }); //update model if(hasBill) { billing.updateUserBalanceCache(ownerId, false); } m.updateAccTariff(accUid, newTariff.id, now, changedInDay, oldTariff.id); //send remote updates sendUpdateOtherFrontsSignalAsync(ReloadChatsModelType.ACC_UPDATED, accUid); log.info("acc tariff updated: uid="+acc.uid +", tariffId="+newTariffId +", oldTariffId="+oldTariff +", req="+getReqInfoStr()); return amount; } @Secured public Map<Long, UserAccInfo> getAccUsers(String accUid) throws Exception { findUserIdFromSecurityContext(); return m.getAccUsers(accUid); } @Secured public Map<Long, UserAccInfo> getAccOperators(String accUid) throws Exception{ findUserIdFromSecurityContext(); return m.getAccOperators(accUid); } @Secured public List<ServerRow> getServers() throws Exception { checkAccessFor_MODERATOR(); return m.getServers(); } @Secured public List<ChatAccount> getServerAccs(long serverId) throws Exception { checkAccessFor_MODERATOR(); return m.getServerAccounts(serverId); } @Secured public Map<String, Long> getAllAccOwners(){ checkAccessFor_MODERATOR(); return m.getAllAccOwners(); } @Secured public void setOperatorForAcc(String accUid, long userId) throws Exception{ addUserPrivileges(accUid, userId, singleton(CHAT_OPERATOR)); } @Secured public void addUserPrivileges(String accUid, long userId, Set<PrivilegeType> privsSet) throws Exception{ if(isEmpty(privsSet)) return; checkAccessForPrivsUpdate(accUid, privsSet); User owner = findUserFromSecurityContext(); UserExt user = universal.selectOne(new SelectUserById(userId)); if(user == null) return; //read model ChatAccount acc = m.getAccount(accUid, true); if(acc == null) return; long accId = acc.id; ServerRow server = acc.server; UserAccInfo info = m.getPrivilegesForAcc(userId, accUid); Set<PrivilegeType> privs = info.privs; boolean isNew = privs.isEmpty(); int oldSize = privs.size(); privs.addAll(privsSet); if(privs.size() == oldSize) return; //check max operator limit if need if( ! props.getBoolVal(toolMode) && privsSet.contains(CHAT_OPERATOR)){ checkAccForPaused(accUid); Tariff tariff = m.findTariff(acc.tariffId); int maxOperators = tariff.maxOperators; if(maxOperators > 0){ Map<Long, UserAccInfo> curOperators = m.getAccOperators(accUid); int newOpsCount = curOperators.size() + 1; if(newOpsCount > maxOperators) throw new OperatorsLimitException(); } } //update db doInSingleTxMode(()->{ if(isNew){ try { universal.update(new CreateChatAccountPrivileges(accId, userId, privs)); }catch (Exception e) { //already exist universal.update(new UpdateChatAccountPrivileges(accId, userId, privs)); } } else { universal.update(new UpdateChatAccountPrivileges(accId, userId, privs)); } //remove add reqs universal.update(new DeleteChatAccountAddReq(userId, accId)); //remote if(privsSet.contains(CHAT_OPERATOR) && isUseRemote(props)){ PutOperatorReq req = new PutOperatorReq(acc.uid, user.id, info.nickname, user.email); postEncryptedJson(props, server.createUrl(URL_CHAT_PUT_OP), req); } }); //update model m.addPrivileges(userId, accId, privsSet); //send remote updates sendUpdateChatServerSessions(server, userId); sendUpdateOtherFrontsSignalAsync(USER_PRIVS_UPDATED, userId, accId); log.info("user privs added: uid="+accUid +", userId="+userId +", privs="+privsSet +", ownerId="+owner.id +", ownerLogin="+owner.login +", req="+getReqInfoStr()); } @Secured public void removeUserPrivileges(String accUid, long userId, Set<PrivilegeType> privsSet) throws Exception{ if(isEmpty(privsSet)) return; checkAccessForPrivsUpdate(accUid, privsSet); User owner = findUserFromSecurityContext(); UserExt user = universal.selectOne(new SelectUserById(userId)); if(user == null) return; //read model ChatAccount acc = m.getAccount(accUid, true); if(acc == null) return; long accId = acc.id; ServerRow server = acc.server; Set<PrivilegeType> privs = getAccPrivilegesForUser(acc.id, userId); boolean isNew = privs.isEmpty(); if(isNew) return; int oldSize = privs.size(); privs.removeAll(privsSet); if(privs.size() == oldSize) return; //update db doInSingleTxMode(()->{ if( ! privs.isEmpty()){ universal.update(new UpdateChatAccountPrivileges(accId, userId, privs)); } else { universal.update(new DeleteChatAccountPrivilege(userId, accId)); } //remote if(privsSet.contains(CHAT_OPERATOR) && isUseRemote(props)){ RemoveOperatorReq req = new RemoveOperatorReq(acc.uid, user); postEncryptedJson(props, server.createUrl(URL_CHAT_REMOVE_OP), req); } }); //update model m.removePrivileges(userId, accId, privsSet); //send remote updates sendUpdateChatServerSessions(server, userId); sendUpdateOtherFrontsSignalAsync(USER_PRIVS_UPDATED, userId, accId); log.info("user privs removed: uid="+accUid +", userId="+userId +", privs="+privsSet +(privs.isEmpty()? ", userRemoved" : "") +", ownerId="+owner.id +", ownerLogin="+owner.login +", req="+getReqInfoStr()); } private void checkAccessForPrivsUpdate(String accUid, Set<PrivilegeType> privsSet) throws AccessDeniedException { if(privsSet.contains(CHAT_OWNER)) checkAccessFor_ADMIN(); else if(privsSet.contains(CHAT_MODER)) checkPrivilegesForAcc_Owner(accUid); else checkPrivilegesForAcc_Owner_Moder(accUid); } @Secured public List<ChatAccount> getAccsWithUserReqs() throws Exception { long userId = findUserIdFromSecurityContext(); //db List<ChatAccountAddReq> reqs = universal.select(new GetAllChatAccountAddReqsByUser(userId)); if(isEmpty(reqs)) return list(); ArrayList<ChatAccount> out = new ArrayList<ChatAccount>(); for(ChatAccountAddReq req : reqs){ ChatAccount acc = m.getAccount(req.accId, false); if(acc != null) { acc.params = map("reqDate", req.created); out.add(acc); } } return out; } @Secured public List<ChatAccountAddReq> getReqsByAcc(String accUid) throws Exception { checkPrivilegesForAcc_Owner_Moder(accUid); ChatAccount acc = m.getAccount(accUid, false); if(acc == null) return list(); //db return universal.select(new GetAllChatAccountAddReqsByAcc(acc.id)); } @Secured public void addUserReqToAcc(String accUid) throws NoChatAccountException, UserAlreadyInAccountException, AddUserReqAlreadyExistsException, Exception { User user = findUserFromSecurityContext(); long userId = user.id; ChatAccount acc = m.findAccount(accUid, false); UserAccInfo info = m.getPrivilegesForAcc(userId, accUid); if( ! isEmpty(info.privs)) throw new UserAlreadyInAccountException(); //db try { universal.update(new CreateChatAccountAddReqs(acc.id, userId)); }catch(SQLException e){ if(containsAnyTextInMessage(e, "userid, accid", "unique index")){ throw new AddUserReqAlreadyExistsException(); } else { throw e; } } log.info("AddUserReq added: accUid="+accUid +", userId="+user.id +", userLogin="+user.login +", req="+getReqInfoStr()); } @Secured public void removeAccAddReqByUser(String accUid) throws Exception{ User user = findUserFromSecurityContext(); long userId = user.id; ChatAccount acc = m.getAccount(accUid, false); if(acc == null) return; //db universal.update(new DeleteChatAccountAddReq(userId, acc.id)); log.info("AddUserReq removed: accUid="+accUid +", userId="+user.id +", userLogin="+user.login +", req="+getReqInfoStr()); } @Secured public void removeAccAddReqForUser(String accUid, long userId)throws Exception{ checkPrivilegesForAcc_Owner_Moder(accUid); User owner = findUserFromSecurityContext(); ChatAccount acc = m.getAccount(accUid, false); if(acc == null) return; universal.update(new DeleteChatAccountAddReq(userId, acc.id)); log.info("AddUserReq removed by owner: accUid="+accUid +", userId="+userId +", ownerId="+owner.id +", onwerLogin="+owner.login +", req="+getReqInfoStr()); } @Secured public boolean isBlockedAcc(String accUid){ findUserIdFromSecurityContext(); return isAccBlockedFromCache(cache, accUid); } public boolean isPausedAcc(String accUid){ findUserIdFromSecurityContext(); ChatAccount acc = m.getAccount(accUid, false); if(acc == null) return false; return acc.tariffId == PAUSE_TARIFF_ID; } @Secured public List<String> getBlockedAccs() { long userId = findUserIdFromSecurityContext(); Set<String> accs = m.getAccountsUidsForAnyPriv(userId); if(isEmpty(accs)) return list(); ArrayList<String> blocked = new ArrayList<String>(); for(String uid : accs){ if(isAccBlockedFromCache(cache, uid)) blocked.add(uid); } return blocked; } @Secured public boolean setNickname(String accUid, String nickname) throws Exception { User user = findUserFromSecurityContext(); long userId = user.id; boolean out = setNicknameInternal(accUid, userId, nickname); log.info("nickname changed: accUid="+accUid +", nickname="+nickname +", userId="+user.id +", userLogin="+user.login +", req="+getReqInfoStr()); return out; } @Secured public boolean setNickname(String accUid, long userId, String nickname) throws Exception { checkPrivilegesForAcc_Owner_Moder(accUid); if(nickname == null) nickname = ""; User owner = findUserFromSecurityContext(); boolean out = setNicknameInternal(accUid, userId, nickname); log.info("nickname changed by owner: accUid="+accUid +", nickname="+nickname +", userId="+userId +", ownerId="+owner.id +", ownerLogin="+owner.login +", req="+getReqInfoStr()); return out; } public Future<?> checkAndLogReferer(HttpServletRequest req, String customRef, String accUid) throws HostBlockedException { ChatAccount acc = m.getAccount(accUid, false); if(acc == null) return EMPTY_DONE_FUTURE; Long accOwner = m.getAccOwner(acc.uid); if(accOwner == null) return EMPTY_DONE_FUTURE; String ref = WebUtil.getReferer(req); if( ! hasText(ref)) ref = customRef; if( ! hasText(ref)) return EMPTY_DONE_FUTURE; URL url = null; try { url = new URL(ref); }catch(Exception e){ return EMPTY_DONE_FUTURE; } String host = url.getHost(); if( ! hasText(host)) return EMPTY_DONE_FUTURE; checkBlockedHost(props, host, accOwner, accUid); //async part Future<Object> f = c.async.invoke(()->{ HostsStat curStat = hostsStat; if(curStat == null) return null; curStat.putStat(host, acc.id, accOwner); return null; }); return f; } @Secured public boolean saveClientsHostsStat() throws Exception{ checkAccessFor_ADMIN(); return saveClientsHostsStatImpl(); } private boolean saveClientsHostsStatImpl() throws Exception { //empty stat if(hostsStat == null) hostsStat = new HostsStat(); if(hostsStat.isEmpty()) return false; //create new stat HostsStat oldStat = hostsStat; hostsStat = new HostsStat(); oldStat.stopUpdating(); Map<String, Set<Long>> accsOwnerByHost = oldStat.getHostsWithOwners(); Map<String, Set<Long>> accsByHost = oldStat.getHostsWithAccs(); //update db for(Entry<String, Set<Long>> entry : accsOwnerByHost.entrySet()){ //create or get host String name = entry.getKey(); ClientHost host = universal.selectOne(new GetClientHost(name)); if(host == null){ try { host = new ClientHost(universal.nextSeqFor(client_hosts), name); host.important = getHostImportantFlag(props, host.name); universal.update(new CreateClientHost(host)); }catch(Exception e){ host = universal.selectOne(new GetClientHost(name)); } } if(host == null) { log.error("can't get client host data by name: "+name); continue; } //owners for(Long owner : entry.getValue()){ try { universal.update(new CreateClientHostAccOwner(host.id, owner)); }catch(Exception e){ //already created } } //accs Set<Long> accs = accsByHost.get(name); if( ! isEmpty(accs)){ for (Long accId : accs) { try { universal.update(new CreateClientHostAcc(host.id, accId)); }catch(Exception e){ //already created } } } } return true; } private boolean setNicknameInternal(String accUid, long userId, String nickname) throws Exception { validateForTextSize(nickname, "nickname", 0, MAX_NICKNAME_SIZE); ChatAccount acc = m.findAccount(accUid, true); long accId = acc.id; UserAccInfo info = m.getPrivilegesForAcc(userId, accUid); //update db int updateCount[] = {0}; doInSingleTxMode(()->{ updateCount[0] = universal.updateOne(new UpdateUserAccNickname(acc.id, userId, nickname)); if(updateCount[0] > 0 && info.privs.contains(CHAT_OPERATOR) && isUseRemote(props)){ PutOperatorReq req = new PutOperatorReq(acc.uid, userId, nickname); postEncryptedJson(props, acc.server.createUrl(URL_CHAT_PUT_OP), req); } }); if(updateCount[0] == 0) return false; //update model boolean result = m.setNickname(accUid, userId, nickname); //send remote updates sendUpdateOtherFrontsSignalAsync(USER_PRIVS_UPDATED, userId, accId); return result; } private void updateRemoteContactsOnUserUpdateTx(UserUpdateTxEvent event) throws Exception { if( ! isUseRemote(props)) return; User user = event.old; UpdateUserReq req = event.req; if( ! isUpdateNotEmptyVal(user.email, req.email)) return; //обновляем email на всех чат-серверах где юзер - оператор Set<String> accsToUpdate = m.getAccountsUidsFor(user.id, CHAT_OPERATOR); if( isEmpty(accsToUpdate)) return; Set<String> serverUrls = new HashSet<String>(); for(String accId : accsToUpdate){ ChatAccount acc = m.getAccount(accId, true); if(acc == null) continue; serverUrls.add(acc.server.createUrl(URL_CHAT_UPDATE_USER_CONTACT)); } postEncryptedJsonToAny(props, serverUrls, new UpdateUserContactReq(user.id, req.email)); log.info("send to accs new user data: " +"userId="+user.id +", accs="+accsToUpdate +", req="+getReqInfoStr()); } private void checkUserAccsForBlocked() throws ChatAccountBlockedException, SQLException { long userId = findUserIdFromSecurityContext(); UserBalance balance = universal.selectOne(new SelectUserBalanceById(userId)); if(balance == null) throw new UserNotFoundException(userId); if(balance.accsBlocked) throw new ChatAccountBlockedException(); } private void checkAccForPaused(String accUid) throws ChatAccountPausedException { ChatAccount acc = m.findAccount(accUid, false); if(acc.tariffId == PAUSE_TARIFF_ID) throw new ChatAccountPausedException(); } private void checkPrivilegesForAcc_Owner(String accUid) throws AccessDeniedException { checkPrivilegesForAcc(accUid, CHAT_OWNER); } private void checkPrivilegesForAcc_Owner_Moder(String accUid) throws AccessDeniedException { checkPrivilegesForAcc(accUid, CHAT_OWNER, CHAT_MODER); } @SuppressWarnings("unused") private void checkPrivilegesForAcc_AnyRole(String accUid)throws AccessDeniedException { checkPrivilegesForAcc(accUid, CHAT_OWNER, CHAT_MODER, CHAT_OPERATOR); } private void checkPrivilegesForAcc(String accUid, PrivilegeType...privs) throws AccessDeniedException { if(hasAccessFor(UserRole.ADMIN)) return; long userId = findUserIdFromSecurityContext(); UserAccInfo info = m.getPrivilegesForAcc(userId, accUid); for (PrivilegeType priv : privs) { if(info.privs.contains(priv)) return; } throw new AccessDeniedException("userId="+userId+", accUid="+accUid+", needPrivs="+list(privs)); } private void sendUpdateOtherFrontsSignalAsync(ReloadChatsModelType type) { sendUpdateOtherFrontsSignalAsync(type, null, null); } private void sendUpdateOtherFrontsSignalAsync(ReloadChatsModelType type, Object param1) { sendUpdateOtherFrontsSignalAsync(type, param1, null); } private void sendUpdateOtherFrontsSignalAsync(ReloadChatsModelType type, Object param1, Object param2) { String serversUrls = c.props.getVal(frontServerUrls); if(isEmpty(serversUrls)) return; final List<String> reqUrls = new ArrayList<>(); for(String url : strToList(serversUrls, " ")){ reqUrls.add(url+URL_SYNC_RELOAD_CHATS_MODELS); } if(isEmpty(reqUrls)) return; c.async.invoke(()-> postEncryptedJsonToAny(props, reqUrls, new ReloadChatsModelReq(c.root.id, type, param1, param2)) ); } private void sendUpdateChatServerSessions(ServerRow server, long userId) { if( ! isUseRemote(props)) return; Map<String, Set<PrivilegeType>> privilegesByAccount = m.getPrivilegesForAccs(userId); try { postEncryptedJson(props, server.createUrl(URL_CHAT_UPDATE_SESSIONS), new UpdateUserSessionsReq(userId, privilegesByAccount)); }catch (Throwable t) { ExpectedException.logError(log, t, "can't updateChatServerSessions"); } } public static class UpdateTariffOps { boolean canUseNotPublic = false; boolean checkMaxChangedInDay = true; Date tariffStartPreset; Date tariffLastPayPreset; Date nowPreset; BigDecimal pricePreset; CallableVoid beforeTxBeginListener; CallableVoid beforeTxCommitListener; public UpdateTariffOps() {} public UpdateTariffOps(boolean canUseNotPublic, boolean checkMaxChangedInDay) { this.canUseNotPublic = canUseNotPublic; this.checkMaxChangedInDay = checkMaxChangedInDay; } public UpdateTariffOps(boolean canUseNotPublic, boolean checkMaxChangedInDay, Date tariffStartPreset, Date tariffLastPayPreset, Date nowPreset, BigDecimal pricePreset) { this.canUseNotPublic = canUseNotPublic; this.checkMaxChangedInDay = checkMaxChangedInDay; this.tariffStartPreset = tariffStartPreset; this.tariffLastPayPreset = tariffLastPayPreset; this.nowPreset = nowPreset; this.pricePreset = pricePreset; } public UpdateTariffOps(boolean canUseNotPublic, boolean checkMaxChangedInDay, Date tariffStartPreset, Date tariffLastPayPreset, Date nowPreset, BigDecimal pricePreset, CallableVoid beforeDbUpdateListener) { this.canUseNotPublic = canUseNotPublic; this.checkMaxChangedInDay = checkMaxChangedInDay; this.tariffStartPreset = tariffStartPreset; this.tariffLastPayPreset = tariffLastPayPreset; this.nowPreset = nowPreset; this.pricePreset = pricePreset; this.beforeTxBeginListener = beforeDbUpdateListener; } } }