MucOptions.java

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