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