MucOptions.java

   1package eu.siacs.conversations.entities;
   2
   3import android.content.Context;
   4import android.net.Uri;
   5import android.text.TextUtils;
   6
   7import androidx.annotation.NonNull;
   8import androidx.annotation.Nullable;
   9
  10import java.util.ArrayList;
  11import java.util.Collections;
  12import java.util.HashSet;
  13import java.util.List;
  14import java.util.Locale;
  15import java.util.Set;
  16
  17import io.ipfs.cid.Cid;
  18
  19import eu.siacs.conversations.Config;
  20import eu.siacs.conversations.R;
  21import eu.siacs.conversations.services.AvatarService;
  22import eu.siacs.conversations.services.MessageArchiveService;
  23import eu.siacs.conversations.utils.JidHelper;
  24import eu.siacs.conversations.utils.UIHelper;
  25import eu.siacs.conversations.xmpp.Jid;
  26import eu.siacs.conversations.xmpp.chatstate.ChatState;
  27import eu.siacs.conversations.xmpp.forms.Data;
  28import eu.siacs.conversations.xmpp.forms.Field;
  29import eu.siacs.conversations.xmpp.pep.Avatar;
  30import eu.siacs.conversations.xml.Element;
  31
  32import java.util.ArrayList;
  33import java.util.Arrays;
  34import java.util.Collections;
  35import java.util.HashSet;
  36import java.util.List;
  37import java.util.Locale;
  38import java.util.Set;
  39
  40public class MucOptions {
  41
  42    public static final String STATUS_CODE_SELF_PRESENCE = "110";
  43    public static final String STATUS_CODE_ROOM_CREATED = "201";
  44    public static final String STATUS_CODE_BANNED = "301";
  45    public static final String STATUS_CODE_CHANGED_NICK = "303";
  46    public static final String STATUS_CODE_KICKED = "307";
  47    public static final String STATUS_CODE_AFFILIATION_CHANGE = "321";
  48    public static final String STATUS_CODE_LOST_MEMBERSHIP = "322";
  49    public static final String STATUS_CODE_SHUTDOWN = "332";
  50    public static final String STATUS_CODE_TECHNICAL_REASONS = "333";
  51    private final Set<User> users = new HashSet<>();
  52    private final Conversation conversation;
  53    public OnRenameListener onRenameListener = null;
  54    private boolean mAutoPushConfiguration = true;
  55    private final Account account;
  56    private ServiceDiscoveryResult serviceDiscoveryResult;
  57    private boolean isOnline = false;
  58    private Error error = Error.NONE;
  59    private User self;
  60    private String password = null;
  61
  62    private boolean tookProposedNickFromBookmark = false;
  63
  64    public MucOptions(Conversation conversation) {
  65        this.account = conversation.getAccount();
  66        this.conversation = conversation;
  67        final String nick = getProposedNick(conversation.getAttribute("mucNick"));
  68        this.self = new User(this, createJoinJid(nick), null, nick, new HashSet<>());
  69        this.self.affiliation = Affiliation.of(conversation.getAttribute("affiliation"));
  70        this.self.role = Role.of(conversation.getAttribute("role"));
  71    }
  72
  73    public Account getAccount() {
  74        return this.conversation.getAccount();
  75    }
  76
  77    public boolean setSelf(User user) {
  78        this.self = user;
  79        final boolean roleChanged = this.conversation.setAttribute("role", user.role.toString());
  80        final boolean affiliationChanged = this.conversation.setAttribute("affiliation", user.affiliation.toString());
  81        this.conversation.setAttribute("mucNick", user.getNick());
  82        return roleChanged || affiliationChanged;
  83    }
  84
  85    public void changeAffiliation(Jid jid, Affiliation affiliation) {
  86        User user = findUserByRealJid(jid);
  87        synchronized (users) {
  88            if (user == null) {
  89                user = new User(this, null, null, null, new HashSet<>());
  90                user.setRealJid(jid);
  91                user.setOnline(false);
  92                users.add(user);
  93            }
  94            user.affiliation = affiliation;
  95        }
  96    }
  97
  98    public void flagNoAutoPushConfiguration() {
  99        mAutoPushConfiguration = false;
 100    }
 101
 102    public boolean autoPushConfiguration() {
 103        return mAutoPushConfiguration;
 104    }
 105
 106    public boolean isSelf(Jid counterpart) {
 107        return counterpart.equals(self.getFullJid());
 108    }
 109
 110    public void resetChatState() {
 111        synchronized (users) {
 112            for (User user : users) {
 113                user.chatState = Config.DEFAULT_CHAT_STATE;
 114            }
 115        }
 116    }
 117
 118    public boolean isTookProposedNickFromBookmark() {
 119        return tookProposedNickFromBookmark;
 120    }
 121
 122    void notifyOfBookmarkNick(final String nick) {
 123        final String normalized = normalize(account.getJid(),nick);
 124        if (normalized != null && normalized.equals(getSelf().getNick())) {
 125            this.tookProposedNickFromBookmark = true;
 126        }
 127    }
 128
 129    public boolean mamSupport() {
 130        return MessageArchiveService.Version.has(getFeatures());
 131    }
 132
 133    public boolean updateConfiguration(ServiceDiscoveryResult serviceDiscoveryResult) {
 134        this.serviceDiscoveryResult = serviceDiscoveryResult;
 135        String name;
 136        Field roomConfigName = getRoomInfoForm().getFieldByName("muc#roomconfig_roomname");
 137        if (roomConfigName != null) {
 138            name = roomConfigName.getValue();
 139        } else {
 140            List<ServiceDiscoveryResult.Identity> identities = serviceDiscoveryResult.getIdentities();
 141            String identityName = identities.size() > 0 ? identities.get(0).getName() : null;
 142            final Jid jid = conversation.getJid();
 143            if (identityName != null && !identityName.equals(jid == null ? null : jid.getEscapedLocal())) {
 144                name = identityName;
 145            } else {
 146                name = null;
 147            }
 148        }
 149        boolean changed = conversation.setAttribute("muc_name", name);
 150        changed |= conversation.setAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, this.hasFeature("muc_membersonly"));
 151        changed |= conversation.setAttribute(Conversation.ATTRIBUTE_MODERATED, this.hasFeature("muc_moderated"));
 152        changed |= conversation.setAttribute(Conversation.ATTRIBUTE_NON_ANONYMOUS, this.hasFeature("muc_nonanonymous"));
 153        return changed;
 154    }
 155
 156    private Data getRoomInfoForm() {
 157        final List<Data> forms = serviceDiscoveryResult == null ? Collections.emptyList() : serviceDiscoveryResult.forms;
 158        return forms.size() == 0 ? new Data() : forms.get(0);
 159    }
 160
 161    public String getAvatar() {
 162        return account.getRoster().getContact(conversation.getJid()).getAvatarFilename();
 163    }
 164
 165    public boolean hasFeature(String feature) {
 166        return this.serviceDiscoveryResult != null && this.serviceDiscoveryResult.features.contains(feature);
 167    }
 168
 169    public boolean hasVCards() {
 170        return hasFeature("vcard-temp");
 171    }
 172
 173    public boolean canInvite() {
 174        final boolean hasPermission = !membersOnly() || self.getRole().ranks(Role.MODERATOR) || allowInvites();
 175        return hasPermission && online();
 176    }
 177
 178    public boolean allowInvites() {
 179        final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowinvites");
 180        return field != null && "1".equals(field.getValue());
 181    }
 182
 183    public boolean canChangeSubject() {
 184        return self.getRole().ranks(Role.MODERATOR) || participantsCanChangeSubject();
 185    }
 186
 187    public boolean participantsCanChangeSubject() {
 188        final Field configField = getRoomInfoForm().getFieldByName("muc#roomconfig_changesubject");
 189        final Field infoField = getRoomInfoForm().getFieldByName("muc#roominfo_changesubject");
 190        final Field field = configField != null ? configField : infoField;
 191        return field != null && "1".equals(field.getValue());
 192    }
 193
 194    public boolean allowPm() {
 195        final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowpm");
 196        if (field == null) {
 197            return true; //fall back if field does not exists
 198        }
 199        if ("anyone".equals(field.getValue())) {
 200            return true;
 201        } else if ("participants".equals(field.getValue())) {
 202            return self.getRole().ranks(Role.PARTICIPANT);
 203        } else if ("moderators".equals(field.getValue())) {
 204            return self.getRole().ranks(Role.MODERATOR);
 205        } else {
 206            return false;
 207        }
 208    }
 209
 210    public boolean allowPmRaw() {
 211        final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowpm");
 212        return  field == null || Arrays.asList("anyone","participants").contains(field.getValue());
 213    }
 214
 215    public boolean participating() {
 216        return self.getRole().ranks(Role.PARTICIPANT) || !moderated();
 217    }
 218
 219    public boolean membersOnly() {
 220        return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, false);
 221    }
 222
 223    public List<String> getFeatures() {
 224        return this.serviceDiscoveryResult != null ? this.serviceDiscoveryResult.features : Collections.emptyList();
 225    }
 226
 227    public boolean nonanonymous() {
 228        return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_NON_ANONYMOUS, false);
 229    }
 230
 231    public boolean isPrivateAndNonAnonymous() {
 232        return membersOnly() && nonanonymous();
 233    }
 234
 235    public boolean moderated() {
 236        return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MODERATED, false);
 237    }
 238
 239    public boolean stableId() {
 240        return getFeatures().contains("http://jabber.org/protocol/muc#stable_id");
 241    }
 242
 243    public User deleteUser(Jid jid) {
 244        User user = findUserByFullJid(jid);
 245        if (user != null) {
 246            synchronized (users) {
 247                users.remove(user);
 248                boolean realJidInMuc = false;
 249                for (User u : users) {
 250                    if (user.realJid != null && user.realJid.equals(u.realJid)) {
 251                        realJidInMuc = true;
 252                        break;
 253                    }
 254                }
 255                boolean self = user.realJid != null && user.realJid.equals(account.getJid().asBareJid());
 256                if (membersOnly()
 257                        && nonanonymous()
 258                        && user.affiliation.ranks(Affiliation.MEMBER)
 259                        && user.realJid != null
 260                        && !realJidInMuc
 261                        && !self) {
 262                    user.role = Role.NONE;
 263                    user.avatar = null;
 264                    user.fullJid = null;
 265                    users.add(user);
 266                }
 267            }
 268        }
 269        return user;
 270    }
 271
 272    //returns true if real jid was new;
 273    public boolean updateUser(User user) {
 274        User old;
 275        boolean realJidFound = false;
 276        if (user.fullJid == null && user.realJid != null) {
 277            old = findUserByRealJid(user.realJid);
 278            realJidFound = old != null;
 279            if (old != null) {
 280                if (old.fullJid != null) {
 281                    return false; //don't add. user already exists
 282                } else {
 283                    synchronized (users) {
 284                        users.remove(old);
 285                    }
 286                }
 287            }
 288        } else if (user.realJid != null) {
 289            old = findUserByRealJid(user.realJid);
 290            realJidFound = old != null;
 291            synchronized (users) {
 292                if (old != null && (old.fullJid == null || old.role == Role.NONE)) {
 293                    users.remove(old);
 294                }
 295            }
 296        }
 297        old = findUserByFullJid(user.getFullJid());
 298
 299        synchronized (this.users) {
 300            if (old != null) {
 301                users.remove(old);
 302                if (old.nick != null && user.nick == null && old.getName().equals(user.getName())) user.nick = old.nick;
 303                if (old.hats != null && user.hats == null) user.hats = old.hats;
 304                if (old.avatar != null && user.avatar == null) user.avatar = old.avatar;
 305            }
 306            boolean fullJidIsSelf = isOnline && user.getFullJid() != null && user.getFullJid().equals(self.getFullJid());
 307            if (!fullJidIsSelf) {
 308                this.users.add(user);
 309                return !realJidFound && user.realJid != null;
 310            }
 311        }
 312        return false;
 313    }
 314
 315    public User findUserByName(final String name) {
 316        if (name == null) {
 317            return null;
 318        }
 319        synchronized (users) {
 320            for (User user : users) {
 321                if (name.equals(user.getName())) {
 322                    return user;
 323                }
 324            }
 325        }
 326        return null;
 327    }
 328
 329    public User findUserByFullJid(Jid jid) {
 330        if (jid == null) {
 331            return null;
 332        }
 333        synchronized (users) {
 334            for (User user : users) {
 335                if (jid.equals(user.getFullJid())) {
 336                    return user;
 337                }
 338            }
 339        }
 340        return null;
 341    }
 342
 343    public User findUserByRealJid(Jid jid) {
 344        if (jid == null) {
 345            return null;
 346        }
 347        synchronized (users) {
 348            for (User user : users) {
 349                if (jid.asBareJid().equals(user.realJid)) {
 350                    return user;
 351                }
 352            }
 353        }
 354        return null;
 355    }
 356
 357    public User findUserByOccupantId(final String id, final Jid counterpart) {
 358        if (id == null) {
 359            return null;
 360        }
 361        synchronized (users) {
 362            for (User user : users) {
 363                if (id.equals(user.getOccupantId())) {
 364                    return user;
 365                }
 366            }
 367        }
 368        final var user = new User(this, counterpart, id, null, new HashSet<>());
 369        user.setOnline(false);
 370        return user;
 371    }
 372
 373    public User findOrCreateUserByRealJid(Jid jid, Jid fullJid, final String occupantId) {
 374        User user = findUserByRealJid(jid);
 375        if (user == null) {
 376            user = new User(this, fullJid, occupantId, null, new HashSet<>());
 377            user.setRealJid(jid);
 378            user.setOnline(false);
 379        }
 380        return user;
 381    }
 382
 383    public User findUser(ReadByMarker readByMarker) {
 384        if (readByMarker.getRealJid() != null) {
 385            return findOrCreateUserByRealJid(readByMarker.getRealJid().asBareJid(), readByMarker.getFullJid(), null);
 386        } else if (readByMarker.getFullJid() != null) {
 387            return findUserByFullJid(readByMarker.getFullJid());
 388        } else {
 389            return null;
 390        }
 391    }
 392
 393    public boolean isContactInRoom(Contact contact) {
 394        return contact != null && isUserInRoom(findUserByRealJid(contact.getJid().asBareJid()));
 395    }
 396
 397    public boolean isUserInRoom(Jid jid) {
 398        return isUserInRoom(findUserByFullJid(jid));
 399    }
 400
 401    public boolean isUserInRoom(User user) {
 402        return user != null && user.isOnline();
 403    }
 404
 405    public boolean setOnline() {
 406        boolean before = this.isOnline;
 407        this.isOnline = true;
 408        return !before;
 409    }
 410
 411    public ArrayList<User> getUsers() {
 412        return getUsers(true);
 413    }
 414
 415    public ArrayList<User> getUsers(boolean includeOffline) {
 416        synchronized (users) {
 417            ArrayList<User> users = new ArrayList<>();
 418            for (User user : this.users) {
 419                if (!user.isDomain() && (includeOffline ? user.getAffiliation().ranks(Affiliation.NONE) : user.getRole().ranks(Role.PARTICIPANT))) {
 420                    users.add(user);
 421                }
 422            }
 423            return users;
 424        }
 425    }
 426
 427    public ArrayList<User> getUsersByRole(Role role) {
 428        synchronized (users) {
 429            ArrayList<User> list = new ArrayList<>();
 430            for (User user : users) {
 431                if (user.getRole().ranks(role)) {
 432                    list.add(user);
 433                }
 434            }
 435            return list;
 436        }
 437    }
 438
 439    public ArrayList<User> getUsersWithChatState(ChatState state, int max) {
 440        synchronized (users) {
 441            ArrayList<User> list = new ArrayList<>();
 442            for (User user : users) {
 443                if (user.chatState == state) {
 444                    list.add(user);
 445                    if (list.size() >= max) {
 446                        break;
 447                    }
 448                }
 449            }
 450            return list;
 451        }
 452    }
 453
 454    public List<User> getUsers(int max) {
 455        ArrayList<User> subset = new ArrayList<>();
 456        HashSet<Jid> jids = new HashSet<>();
 457        jids.add(account.getJid().asBareJid());
 458        synchronized (users) {
 459            for (User user : users) {
 460                if (user.getRealJid() == null || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) {
 461                    subset.add(user);
 462                }
 463                if (subset.size() >= max) {
 464                    break;
 465                }
 466            }
 467        }
 468        return subset;
 469    }
 470
 471    public static List<User> sub(List<User> users, int max) {
 472        ArrayList<User> subset = new ArrayList<>();
 473        HashSet<Jid> jids = new HashSet<>();
 474        for (User user : users) {
 475            jids.add(user.getAccount().getJid().asBareJid());
 476            if (user.getRealJid() == null || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) {
 477                subset.add(user);
 478            }
 479            if (subset.size() >= max) {
 480                break;
 481            }
 482        }
 483        return subset;
 484    }
 485
 486    public int getUserCount() {
 487        synchronized (users) {
 488            return users.size();
 489        }
 490    }
 491
 492    public String getProposedNick() {
 493        return getProposedNick(null);
 494    }
 495
 496    public String getProposedNick(final String mucNick) {
 497        final Bookmark bookmark = this.conversation.getBookmark();
 498        final String bookmarkedNick = normalize(account.getJid(), bookmark == null ? null : bookmark.getNick());
 499        if (bookmarkedNick != null) {
 500            this.tookProposedNickFromBookmark = true;
 501            return bookmarkedNick;
 502        } else if (mucNick != null) {
 503            return mucNick;
 504        } else if (!conversation.getJid().isBareJid()) {
 505            return conversation.getJid().getResource();
 506        } else {
 507            return defaultNick(account);
 508        }
 509    }
 510
 511    public static String defaultNick(final Account account) {
 512        final String displayName = normalize(account.getJid(), account.getDisplayName());
 513        if (displayName == null) {
 514            return JidHelper.localPartOrFallback(account.getJid());
 515        } else {
 516            return displayName;
 517        }
 518    }
 519
 520    private static String normalize(Jid account, String nick) {
 521        if (account == null || TextUtils.isEmpty(nick)) {
 522            return null;
 523        }
 524
 525        try {
 526            return account.withResource(nick).getResource();
 527        } catch (IllegalArgumentException e) {
 528            return nick;
 529        }
 530    }
 531
 532    public String getActualNick() {
 533        if (this.self.getNick() != null) {
 534            return this.self.getNick();
 535        } else {
 536            return this.getProposedNick();
 537        }
 538    }
 539
 540    public String getActualName() {
 541        if (this.self.getName() != null) {
 542            return this.self.getName();
 543        } else {
 544            return this.getProposedNick();
 545        }
 546    }
 547
 548    public boolean online() {
 549        return this.isOnline;
 550    }
 551
 552    public Error getError() {
 553        return this.error;
 554    }
 555
 556    public void setError(Error error) {
 557        this.isOnline = isOnline && error == Error.NONE;
 558        this.error = error;
 559    }
 560
 561    public void setOnRenameListener(OnRenameListener listener) {
 562        this.onRenameListener = listener;
 563    }
 564
 565    public void setOffline() {
 566        synchronized (users) {
 567            this.users.clear();
 568        }
 569        this.error = Error.NO_RESPONSE;
 570        this.isOnline = false;
 571    }
 572
 573    public User getSelf() {
 574        return self;
 575    }
 576
 577    public boolean setSubject(String subject) {
 578        return this.conversation.setAttribute("subject", subject);
 579    }
 580
 581    public String getSubject() {
 582        return this.conversation.getAttribute("subject");
 583    }
 584
 585    public String getName() {
 586        return this.conversation.getAttribute("muc_name");
 587    }
 588
 589    private List<User> getFallbackUsersFromCryptoTargets() {
 590        List<User> users = new ArrayList<>();
 591        for (Jid jid : conversation.getAcceptedCryptoTargets()) {
 592            User user = new User(this, null, null, null, new HashSet<>());
 593            user.setRealJid(jid);
 594            users.add(user);
 595        }
 596        return users;
 597    }
 598
 599    public List<User> getUsersRelevantForNameAndAvatar() {
 600        final List<User> users;
 601        if (isOnline) {
 602            users = getUsers(5);
 603        } else {
 604            users = getFallbackUsersFromCryptoTargets();
 605        }
 606        return users;
 607    }
 608
 609    String createNameFromParticipants() {
 610        List<User> users = getUsersRelevantForNameAndAvatar();
 611        if (users.size() >= 2) {
 612            StringBuilder builder = new StringBuilder();
 613            for (User user : users) {
 614                if (builder.length() != 0) {
 615                    builder.append(", ");
 616                }
 617                String name = UIHelper.getDisplayName(user);
 618                if (name != null) {
 619                    builder.append(name.split("\\s+")[0]);
 620                }
 621            }
 622            return builder.toString();
 623        } else {
 624            return null;
 625        }
 626    }
 627
 628    public long[] getPgpKeyIds() {
 629        List<Long> ids = new ArrayList<>();
 630        for (User user : this.users) {
 631            if (user.getPgpKeyId() != 0) {
 632                ids.add(user.getPgpKeyId());
 633            }
 634        }
 635        ids.add(account.getPgpId());
 636        long[] primitiveLongArray = new long[ids.size()];
 637        for (int i = 0; i < ids.size(); ++i) {
 638            primitiveLongArray[i] = ids.get(i);
 639        }
 640        return primitiveLongArray;
 641    }
 642
 643    public boolean pgpKeysInUse() {
 644        synchronized (users) {
 645            for (User user : users) {
 646                if (user.getPgpKeyId() != 0) {
 647                    return true;
 648                }
 649            }
 650        }
 651        return false;
 652    }
 653
 654    public boolean everybodyHasKeys() {
 655        synchronized (users) {
 656            for (User user : users) {
 657                if (user.getPgpKeyId() == 0) {
 658                    return false;
 659                }
 660            }
 661        }
 662        return true;
 663    }
 664
 665    public Jid createJoinJid(String nick) {
 666        return createJoinJid(nick, true);
 667    }
 668
 669    private Jid createJoinJid(String nick, boolean tryFix) {
 670        try {
 671            return conversation.getJid().withResource(nick);
 672        } catch (final IllegalArgumentException e) {
 673            try {
 674                return tryFix ? createJoinJid(gnu.inet.encoding.Punycode.encode(nick), false) : null;
 675            } catch (final Exception e2) {
 676                return null;
 677            }
 678        }
 679    }
 680
 681    public Jid getTrueCounterpart(Jid jid) {
 682        if (jid.equals(getSelf().getFullJid())) {
 683            return account.getJid().asBareJid();
 684        }
 685        User user = findUserByFullJid(jid);
 686        return user == null ? null : user.realJid;
 687    }
 688
 689    public String getPassword() {
 690        this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
 691        if (this.password == null && conversation.getBookmark() != null
 692                && conversation.getBookmark().getPassword() != null) {
 693            return conversation.getBookmark().getPassword();
 694        } else {
 695            return this.password;
 696        }
 697    }
 698
 699    public void setPassword(String password) {
 700        if (conversation.getBookmark() != null) {
 701            conversation.getBookmark().setPassword(password);
 702        } else {
 703            this.password = password;
 704        }
 705        conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
 706    }
 707
 708    public Conversation getConversation() {
 709        return this.conversation;
 710    }
 711
 712    public List<Jid> getMembers(final boolean includeDomains) {
 713        ArrayList<Jid> members = new ArrayList<>();
 714        synchronized (users) {
 715            for (User user : users) {
 716                if (user.affiliation.ranks(Affiliation.MEMBER) && user.realJid != null && !user.realJid.asBareJid().equals(conversation.account.getJid().asBareJid()) && (!user.isDomain() || includeDomains)) {
 717                    members.add(user.realJid);
 718                }
 719            }
 720        }
 721        return members;
 722    }
 723
 724    public enum Affiliation {
 725        OWNER(4, R.string.owner),
 726        ADMIN(3, R.string.admin),
 727        MEMBER(2, R.string.member),
 728        OUTCAST(0, R.string.outcast),
 729        NONE(1, R.string.no_affiliation);
 730
 731        private final int resId;
 732        private final int rank;
 733
 734        Affiliation(int rank, int resId) {
 735            this.resId = resId;
 736            this.rank = rank;
 737        }
 738
 739        public static Affiliation of(@Nullable String value) {
 740            if (value == null) {
 741                return NONE;
 742            }
 743            try {
 744                return Affiliation.valueOf(value.toUpperCase(Locale.US));
 745            } catch (IllegalArgumentException e) {
 746                return NONE;
 747            }
 748        }
 749
 750        public int getResId() {
 751            return resId;
 752        }
 753
 754        @Override
 755        public String toString() {
 756            return name().toLowerCase(Locale.US);
 757        }
 758
 759        public boolean outranks(Affiliation affiliation) {
 760            return rank > affiliation.rank;
 761        }
 762
 763        public boolean ranks(Affiliation affiliation) {
 764            return rank >= affiliation.rank;
 765        }
 766    }
 767
 768    public enum Role {
 769        MODERATOR(R.string.moderator, 3),
 770        VISITOR(R.string.visitor, 1),
 771        PARTICIPANT(R.string.participant, 2),
 772        NONE(R.string.no_role, 0);
 773
 774        private final int resId;
 775        private final int rank;
 776
 777        Role(int resId, int rank) {
 778            this.resId = resId;
 779            this.rank = rank;
 780        }
 781
 782        public static Role of(@Nullable String value) {
 783            if (value == null) {
 784                return NONE;
 785            }
 786            try {
 787                return Role.valueOf(value.toUpperCase(Locale.US));
 788            } catch (IllegalArgumentException e) {
 789                return NONE;
 790            }
 791        }
 792
 793        public int getResId() {
 794            return resId;
 795        }
 796
 797        @Override
 798        public String toString() {
 799            return name().toLowerCase(Locale.US);
 800        }
 801
 802        public boolean ranks(Role role) {
 803            return rank >= role.rank;
 804        }
 805    }
 806
 807    public enum Error {
 808        NO_RESPONSE,
 809        SERVER_NOT_FOUND,
 810        REMOTE_SERVER_TIMEOUT,
 811        NONE,
 812        NICK_IN_USE,
 813        PASSWORD_REQUIRED,
 814        BANNED,
 815        MEMBERS_ONLY,
 816        RESOURCE_CONSTRAINT,
 817        KICKED,
 818        SHUTDOWN,
 819        DESTROYED,
 820        INVALID_NICK,
 821        TECHNICAL_PROBLEMS,
 822        UNKNOWN,
 823        NON_ANONYMOUS
 824    }
 825
 826    private interface OnEventListener {
 827        void onSuccess();
 828
 829        void onFailure();
 830    }
 831
 832    public interface OnRenameListener extends OnEventListener {
 833
 834    }
 835
 836    public static class Hat implements Comparable<Hat> {
 837        private final Uri uri;
 838        private final String title;
 839
 840        public Hat(final Element el) {
 841            Uri parseUri = null; // null hat uri is invaild per spec
 842            try {
 843                parseUri = Uri.parse(el.getAttribute("uri"));
 844            } catch (final Exception e) { }
 845            uri = parseUri;
 846
 847            title = el.getAttribute("title");
 848        }
 849
 850        public Hat(final Uri uri, final String title) {
 851            this.uri = uri;
 852            this.title = title;
 853        }
 854
 855        public String toString() {
 856            return title == null ? "" : title;
 857        }
 858
 859        public int getColor() {
 860            return UIHelper.getColorForName(uri == null ? toString() : uri.toString());
 861        }
 862
 863        @Override
 864        public int compareTo(@NonNull Hat another) {
 865            return toString().compareTo(another.toString());
 866        }
 867    }
 868
 869    public static class User implements Comparable<User>, AvatarService.Avatarable {
 870        private Role role = Role.NONE;
 871        private Affiliation affiliation = Affiliation.NONE;
 872        private Jid realJid;
 873        private Jid fullJid;
 874        protected String nick;
 875        private long pgpKeyId = 0;
 876        protected Avatar avatar;
 877        private final MucOptions options;
 878        private ChatState chatState = Config.DEFAULT_CHAT_STATE;
 879        protected Set<Hat> hats;
 880        protected String occupantId;
 881        protected boolean online = true;
 882
 883        public User(MucOptions options, Jid fullJid, final String occupantId, final String nick, final Set<Hat> hats) {
 884            this.options = options;
 885            this.fullJid = fullJid;
 886            this.occupantId = occupantId;
 887            this.nick = nick;
 888            this.hats = hats;
 889
 890            if (occupantId != null && options != null) {
 891                final var sha1sum = options.getConversation().getAttribute("occupantAvatar/" + occupantId);
 892                if (sha1sum != null) {
 893                    avatar = new Avatar();
 894                    avatar.sha1sum = sha1sum;
 895                    avatar.owner = fullJid;
 896                }
 897
 898                if (nick == null) {
 899                    this.nick = options.getConversation().getAttribute("occupantNick/" + occupantId);
 900                } else if (!getNick().equals(getName())) {
 901                    options.getConversation().setAttribute("occupantNick/" + occupantId, nick);
 902                } else {
 903                    options.getConversation().setAttribute("occupantNick/" + occupantId, (String) null);
 904                }
 905            }
 906        }
 907
 908        public String getName() {
 909            return fullJid == null ? null : fullJid.getResource();
 910        }
 911
 912        public Jid getMuc() {
 913            return fullJid == null ? (options.getConversation().getJid().asBareJid()) : fullJid.asBareJid();
 914        }
 915
 916        public String getOccupantId() {
 917            return occupantId;
 918        }
 919
 920        public String getNick() {
 921            return nick == null ? getName() : nick;
 922        }
 923
 924        public void setOnline(final boolean o) {
 925            online = o;
 926        }
 927
 928        public boolean isOnline() {
 929            return fullJid != null && online;
 930        }
 931
 932        public Role getRole() {
 933            return this.role;
 934        }
 935
 936        public void setRole(String role) {
 937            this.role = Role.of(role);
 938        }
 939
 940        public Affiliation getAffiliation() {
 941            return this.affiliation;
 942        }
 943
 944        public void setAffiliation(String affiliation) {
 945            this.affiliation = Affiliation.of(affiliation);
 946        }
 947
 948        public Set<Hat> getHats() {
 949            return this.hats == null ? new HashSet<>() : hats;
 950        }
 951
 952        public List<MucOptions.Hat> getPseudoHats(Context context) {
 953            List<MucOptions.Hat> hats = new ArrayList<>();
 954            if (getAffiliation() != MucOptions.Affiliation.NONE) {
 955                hats.add(new MucOptions.Hat(null, context.getString(getAffiliation().getResId())));
 956            }
 957            if (getRole() != MucOptions.Role.PARTICIPANT) {
 958                hats.add(new MucOptions.Hat(null, context.getString(getRole().getResId())));
 959            }
 960            return hats;
 961        }
 962
 963        public long getPgpKeyId() {
 964            if (this.pgpKeyId != 0) {
 965                return this.pgpKeyId;
 966            } else if (realJid != null) {
 967                return getAccount().getRoster().getContact(realJid).getPgpKeyId();
 968            } else {
 969                return 0;
 970            }
 971        }
 972
 973        public void setPgpKeyId(long id) {
 974            this.pgpKeyId = id;
 975        }
 976
 977        public Contact getContact() {
 978            if (fullJid != null) {
 979                return getAccount().getRoster().getContactFromContactList(realJid);
 980            } else if (realJid != null) {
 981                return getAccount().getRoster().getContact(realJid);
 982            } else {
 983                return null;
 984            }
 985        }
 986
 987        public boolean setAvatar(Avatar avatar) {
 988            if (occupantId != null) {
 989                options.getConversation().setAttribute("occupantAvatar/" + occupantId, getContact() == null && avatar != null ? avatar.sha1sum : null);
 990            }
 991            if (this.avatar != null && this.avatar.equals(avatar)) {
 992                return false;
 993            } else {
 994                this.avatar = avatar;
 995                return true;
 996            }
 997        }
 998
 999        public String getAvatar() {
1000            if (avatar != null) {
1001                return avatar.getFilename();
1002            }
1003            Avatar avatar = realJid != null ? getAccount().getRoster().getContact(realJid).getAvatar() : null;
1004            return avatar == null ? null : avatar.getFilename();
1005        }
1006
1007        public Cid getAvatarCid() {
1008            if (avatar != null) {
1009                return avatar.cid();
1010            }
1011            Avatar avatar = realJid != null ? getAccount().getRoster().getContact(realJid).getAvatar() : null;
1012            return avatar == null ? null : avatar.cid();
1013        }
1014
1015        public Account getAccount() {
1016            return options.getAccount();
1017        }
1018
1019        public Conversation getConversation() {
1020            return options.getConversation();
1021        }
1022
1023        public Jid getFullJid() {
1024            return fullJid;
1025        }
1026
1027        @Override
1028        public boolean equals(Object o) {
1029            if (this == o) return true;
1030            if (o == null || getClass() != o.getClass()) return false;
1031
1032            User user = (User) o;
1033
1034            if (role != user.role) return false;
1035            if (affiliation != user.affiliation) return false;
1036            if (realJid != null ? !realJid.equals(user.realJid) : user.realJid != null)
1037                return false;
1038            return fullJid != null ? fullJid.equals(user.fullJid) : user.fullJid == null;
1039
1040        }
1041
1042        public boolean isDomain() {
1043            return realJid != null && realJid.getLocal() == null && role == Role.NONE;
1044        }
1045
1046        @Override
1047        public int hashCode() {
1048            int result = role != null ? role.hashCode() : 0;
1049            result = 31 * result + (affiliation != null ? affiliation.hashCode() : 0);
1050            result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
1051            result = 31 * result + (fullJid != null ? fullJid.hashCode() : 0);
1052            return result;
1053        }
1054
1055        @Override
1056        public String toString() {
1057            return "[fulljid:" + fullJid + ",realjid:" + realJid + ",nick:" + nick + ",affiliation" + affiliation.toString() + "]";
1058        }
1059
1060        public boolean realJidMatchesAccount() {
1061            return realJid != null && realJid.equals(options.account.getJid().asBareJid());
1062        }
1063
1064        @Override
1065        public int compareTo(@NonNull User another) {
1066            final var anotherPseudoId = another.getOccupantId() != null && another.getOccupantId().charAt(0) == '\0';
1067            final var pseudoId = getOccupantId() != null && getOccupantId().charAt(0) == '\0';
1068            if (anotherPseudoId && !pseudoId) {
1069                return 1;
1070            }
1071            if (pseudoId && !anotherPseudoId) {
1072                return -1;
1073            }
1074            if (another.getAffiliation().outranks(getAffiliation())) {
1075                return 1;
1076            } else if (getAffiliation().outranks(another.getAffiliation())) {
1077                return -1;
1078            } else {
1079                return getComparableName().compareToIgnoreCase(another.getComparableName());
1080            }
1081        }
1082
1083        public String getComparableName() {
1084            Contact contact = getContact();
1085            if (contact != null) {
1086                return contact.getDisplayName();
1087            } else {
1088                String name = getName();
1089                return name == null ? "" : name;
1090            }
1091        }
1092
1093        public Jid getRealJid() {
1094            return realJid;
1095        }
1096
1097        public void setRealJid(Jid jid) {
1098            this.realJid = jid != null ? jid.asBareJid() : null;
1099        }
1100
1101        public boolean setChatState(ChatState chatState) {
1102            if (this.chatState == chatState) {
1103                return false;
1104            }
1105            this.chatState = chatState;
1106            return true;
1107        }
1108
1109        @Override
1110        public int getAvatarBackgroundColor() {
1111            final String seed = realJid != null ? realJid.asBareJid().toString() : null;
1112            return UIHelper.getColorForName(seed == null ? getName() : seed);
1113        }
1114
1115        @Override
1116        public String getAvatarName() {
1117            return getConversation().getName().toString();
1118        }
1119    }
1120}