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