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