/* * Kontalk Java client * Copyright (C) 2016 Kontalk Devteam <devteam@kontalk.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kontalk.system; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.logging.Logger; import java.util.stream.Collectors; import org.kontalk.misc.JID; import org.kontalk.model.Contact; import org.kontalk.model.Model; import org.kontalk.model.chat.GroupChat; import org.kontalk.model.chat.GroupChat.KonGroupChat; import org.kontalk.model.chat.GroupMetaData; import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.model.chat.Member; import org.kontalk.model.chat.ProtoMember; import org.kontalk.model.message.MessageContent; import org.kontalk.model.message.MessageContent.GroupCommand; import org.kontalk.util.EncodingUtils; /** * Control logic for group chat management. * * @author Alexander Bikadorov {@literal <bikaejkb@mail.tu-berlin.de>} */ final class GroupControl { private static final Logger LOGGER = Logger.getLogger(GroupControl.class.getName()); private final Control mControl; private final Model mModel; GroupControl(Control control, Model model) { mControl = control; mModel = model; } abstract class ChatControl<C extends GroupChat> { final C mChat; private ChatControl(C chat) { mChat = chat; } abstract void onCreate(); abstract void onSetSubject(String subject); abstract void onLeave(); abstract boolean beforeDelete(); // abstract void onInMessage(GroupCommand command, Contact sender); } final class KonChatControl extends ChatControl<KonGroupChat> { private KonChatControl(KonGroupChat chat) { super(chat); } @Override void onCreate() { // send create group command List<JID> jids = mChat.getValidContacts().stream() .map(Contact::getJID) .collect(Collectors.toList()); mControl.createAndSendMessage(mChat, MessageContent.groupCommand( MessageContent.GroupCommand.create( jids, mChat.getSubject()) ) ); } @Override public void onSetSubject(String subject) { if (!mChat.isAdministratable()) { LOGGER.warning("not admin"); return; } GroupCommand command = GroupCommand.set(subject); mControl.createAndSendMessage(mChat, MessageContent.groupCommand(command)); applyMyCommand(command); } // NOTE: after we left, group members are actually unknown cause we // don't get any change notifications anymmore. @Override void onLeave() { GroupCommand command = GroupCommand.leave(); mControl.createAndSendMessage(mChat, MessageContent.groupCommand(command)); // NOTE: ignoring if message was sent/received or not applyMyCommand(command); } @Override public boolean beforeDelete() { if (!mChat.isValid()) return true; // TODO if encryption is forced for one member but there is no key, // chat cannot be deleted GroupCommand command = GroupCommand.leave(); // NOTE: group chats are not deleted remotely, were just leaving them boolean succ = mControl.createAndSendMessage(mChat, MessageContent.groupCommand(command)); if (!succ) { // message wasn't send (e.g. not connected), apply leave and // TODO delete chat when command was sent applyMyCommand(command); } return succ; } private void applyMyCommand(GroupCommand command) { Contact me = mModel.contacts().getMe().orElse(null); if (me == null) { LOGGER.warning("no me"); return; } onInMessage(command, me); } @Override public void onInMessage(GroupCommand command, Contact sender) { // TODO ignore message if it contains unexpected group command (?) // NOTE: chat was selected/created by GID so we can be sure message // and chat GIDs match KonGroupData gid = mChat.getGroupData(); MessageContent.GroupCommand.OP op = command.getOperation(); // validation check if (op != GroupCommand.OP.LEAVE) { // sender must be owner if (!gid.owner.equals(sender.getJID())) { LOGGER.warning("sender not owner"); return; } } // apply group command List<ProtoMember> added = new ArrayList<>(); List<ProtoMember> removed = new ArrayList<>(); String subject = ""; switch(command.getOperation()) { case CREATE: //assert mMemberSet.size() == 1; //assert mMemberSet.contains(new Member(sender)); added.addAll(JIDsToMembers(command.getAdded())); if (!added.stream().anyMatch(m -> m.getContact().isMe())) LOGGER.warning("user not included in new chat"); subject = command.getSubject(); break; case LEAVE: removed.add(new ProtoMember(sender)); break; case SET: added.addAll(JIDsToMembers(command.getAdded())); if (command.isAddingMe()) added.addAll(JIDsToMembers(command.getUnchanged())); for (JID jid : command.getRemoved()) { Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.warning("can't get removed contact, jid="+jid); continue; } removed.add(new ProtoMember(contact)); } subject = command.getSubject(); break; default: LOGGER.warning("unhandled operation: "+command.getOperation()); } mChat.applyGroupChanges(added, removed, subject); } } private List<ProtoMember> JIDsToMembers(List<JID> jids) { List<ProtoMember> members = new ArrayList<>(); for (JID jid: jids) { // add contacts if necessary // TODO design problem here: we need at least the public keys, but user // might dont wanna have group members in contact list Contact contact = mControl.getOrCreateContact(jid).orElse(null); if (contact == null) { LOGGER.warning("can't get contact, jid: "+jid); continue; } members.add(new ProtoMember(contact)); } return members; } ChatControl getInstanceFor(GroupChat chat) { if (chat instanceof KonGroupChat) return new KonChatControl((KonGroupChat) chat); throw new IllegalArgumentException("Not implemented for " + chat); } Optional<GroupChat> getGroupChat(GroupMetaData metaData, Contact sender, Optional<GroupCommand> command) { // get old... GroupChat chat = mModel.chats().get(metaData).orElse(null); if (chat != null) { if (!chat.getAllContacts().contains(sender)) { LOGGER.warning("chat does not include sender: "+chat); // TODO we should ask owner to confirm member list return Optional.empty(); } return Optional.of(chat); } // ...or create new if (!(metaData instanceof KonGroupData)) { return Optional.empty(); // creation only for Kontalk Groups } KonGroupData konData = (KonGroupData) metaData; if (!konData.owner.equals(sender.getJID())) { LOGGER.warning("sender is not owner for new group chat: "+metaData); return Optional.empty(); } if (!command.isPresent() || !command.get().isAddingMe()) { LOGGER.warning("ignoring unexpected message of unknown group"); return Optional.empty(); } return Optional.of( mModel.chats().create( Collections.singletonList(new ProtoMember(sender, Member.Role.OWNER)), metaData)); } static KonGroupData newKonGroupData(JID myJID) { return new KonGroupData(myJID, EncodingUtils.randomString(8)); } }