MucOptions.java

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