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        if (users.size() < max) return users;
 572
 573        final var subset = new ArrayList<User>();
 574        final var addresses = new HashSet<Jid>();
 575        for (final var user : users) {
 576            addresses.add(user.getAccount().getJid().asBareJid());
 577            final var address = user.getRealJid();
 578            if (address == null || (address.getLocal() != null && addresses.add(address))) {
 579                subset.add(user);
 580            }
 581            if (subset.size() >= max) {
 582                return subset;
 583            }
 584        }
 585        return subset;
 586    }
 587
 588    public int getUserCount() {
 589        synchronized (users) {
 590            return users.size();
 591        }
 592    }
 593
 594    private String getProposedNick() {
 595        return getProposedNick(null);
 596    }
 597
 598    private String getProposedNick(final String mucNick) {
 599        final Bookmark bookmark = this.conversation.getBookmark();
 600        if (bookmark != null) {
 601            // if we already have a bookmark we consider this the source of truth
 602            return getProposedNickPure();
 603        }
 604        final var storedJid = conversation.getJid();
 605        if (mucNick != null) {
 606            return mucNick;
 607        } else if (storedJid.isBareJid()) {
 608            return defaultNick(account);
 609        } else {
 610            return storedJid.getResource();
 611        }
 612    }
 613
 614    public String getProposedNickPure() {
 615        final Bookmark bookmark = this.conversation.getBookmark();
 616        final String bookmarkedNick =
 617                normalize(account.getJid(), bookmark == null ? null : bookmark.getNick());
 618        if (bookmarkedNick != null) {
 619            return bookmarkedNick;
 620        } else {
 621            return defaultNick(account);
 622        }
 623    }
 624
 625    public static String defaultNick(final Account account) {
 626        final String displayName = normalize(account.getJid(), account.getDisplayName());
 627        if (displayName == null) {
 628            return JidHelper.localPartOrFallback(account.getJid());
 629        } else {
 630            return displayName;
 631        }
 632    }
 633
 634    private static String normalize(final Jid account, final String nick) {
 635        if (account == null || Strings.isNullOrEmpty(nick)) {
 636            return null;
 637        }
 638
 639        try {
 640            return account.withResource(nick).getResource();
 641        } catch (final IllegalArgumentException e) {
 642            return null;
 643        }
 644    }
 645
 646    public String getActualNick() {
 647        if (this.self.getNick() != null) {
 648            return this.self.getNick();
 649        } else {
 650            return this.getProposedNick();
 651        }
 652    }
 653
 654    public String getActualName() {
 655        if (this.self.getName() != null) {
 656            return this.self.getName();
 657        } else {
 658            return this.getProposedNick();
 659        }
 660    }
 661
 662    public boolean online() {
 663        return this.isOnline;
 664    }
 665
 666    public Error getError() {
 667        return this.error;
 668    }
 669
 670    public void setError(Error error) {
 671        this.isOnline = isOnline && error == Error.NONE;
 672        this.error = error;
 673    }
 674
 675    public void setOnRenameListener(OnRenameListener listener) {
 676        this.onRenameListener = listener;
 677    }
 678
 679    public void setOffline() {
 680        synchronized (users) {
 681            this.users.clear();
 682        }
 683        this.error = Error.NO_RESPONSE;
 684        this.isOnline = false;
 685    }
 686
 687    public User getSelf() {
 688        return self;
 689    }
 690
 691    public boolean setSubject(String subject) {
 692        return this.conversation.setAttribute("subject", subject);
 693    }
 694
 695    public String getSubject() {
 696        return this.conversation.getAttribute("subject");
 697    }
 698
 699    public String getName() {
 700        return this.conversation.getAttribute("muc_name");
 701    }
 702
 703    private List<User> getFallbackUsersFromCryptoTargets() {
 704        List<User> users = new ArrayList<>();
 705        for (Jid jid : conversation.getAcceptedCryptoTargets()) {
 706            User user = new User(this, null, null, null, new HashSet<>());
 707            user.setRealJid(jid);
 708            users.add(user);
 709        }
 710        return users;
 711    }
 712
 713    public List<User> getUsersRelevantForNameAndAvatar() {
 714        final List<User> users;
 715        if (isOnline) {
 716            users = getUsers(5);
 717        } else {
 718            users = getFallbackUsersFromCryptoTargets();
 719        }
 720        return users;
 721    }
 722
 723    String createNameFromParticipants() {
 724        List<User> users = getUsersRelevantForNameAndAvatar();
 725        if (users.size() >= 2) {
 726            StringBuilder builder = new StringBuilder();
 727            for (User user : users) {
 728                if (builder.length() != 0) {
 729                    builder.append(", ");
 730                }
 731                String name = UIHelper.getDisplayName(user);
 732                if (name != null) {
 733                    builder.append(name.split("\\s+")[0]);
 734                }
 735            }
 736            return builder.toString();
 737        } else {
 738            return null;
 739        }
 740    }
 741
 742    public long[] getPgpKeyIds() {
 743        List<Long> ids = new ArrayList<>();
 744        for (User user : this.users) {
 745            if (user.getPgpKeyId() != 0) {
 746                ids.add(user.getPgpKeyId());
 747            }
 748        }
 749        ids.add(account.getPgpId());
 750        long[] primitiveLongArray = new long[ids.size()];
 751        for (int i = 0; i < ids.size(); ++i) {
 752            primitiveLongArray[i] = ids.get(i);
 753        }
 754        return primitiveLongArray;
 755    }
 756
 757    public boolean pgpKeysInUse() {
 758        synchronized (users) {
 759            for (User user : users) {
 760                if (user.getPgpKeyId() != 0) {
 761                    return true;
 762                }
 763            }
 764        }
 765        return false;
 766    }
 767
 768    public boolean everybodyHasKeys() {
 769        synchronized (users) {
 770            for (User user : users) {
 771                if (user.getPgpKeyId() == 0) {
 772                    return false;
 773                }
 774            }
 775        }
 776        return true;
 777    }
 778
 779    public Jid createJoinJid(String nick) {
 780        return createJoinJid(nick, true);
 781    }
 782
 783    private Jid createJoinJid(String nick, boolean tryFix) {
 784        try {
 785            return conversation.getJid().withResource(nick);
 786        } catch (final IllegalArgumentException e) {
 787            try {
 788                return tryFix ? createJoinJid(gnu.inet.encoding.Punycode.encode(nick), false) : null;
 789            } catch (final Exception e2) {
 790                return null;
 791            }
 792        }
 793    }
 794
 795    public Jid getTrueCounterpart(Jid jid) {
 796        if (jid.equals(getSelf().getFullJid())) {
 797            return account.getJid().asBareJid();
 798        }
 799        User user = findUserByFullJid(jid);
 800        return user == null ? null : user.realJid;
 801    }
 802
 803    public String getPassword() {
 804        this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
 805        if (this.password == null
 806                && conversation.getBookmark() != null
 807                && conversation.getBookmark().getPassword() != null) {
 808            return conversation.getBookmark().getPassword();
 809        } else {
 810            return this.password;
 811        }
 812    }
 813
 814    public void setPassword(String password) {
 815        if (conversation.getBookmark() != null) {
 816            conversation.getBookmark().setPassword(password);
 817        } else {
 818            this.password = password;
 819        }
 820        conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
 821    }
 822
 823    public Conversation getConversation() {
 824        return this.conversation;
 825    }
 826
 827    public List<Jid> getMembers(final boolean includeDomains) {
 828        ArrayList<Jid> members = new ArrayList<>();
 829        synchronized (users) {
 830            for (User user : users) {
 831                if (user.ranks(Affiliation.MEMBER)
 832                        && user.realJid != null
 833                        && !user.realJid
 834                                .asBareJid()
 835                                .equals(conversation.account.getJid().asBareJid())
 836                        && (!user.isDomain() || includeDomains)) {
 837                    members.add(user.realJid);
 838                }
 839            }
 840        }
 841        return members;
 842    }
 843
 844    public enum Error {
 845        NO_RESPONSE,
 846        SERVER_NOT_FOUND,
 847        REMOTE_SERVER_TIMEOUT,
 848        NONE,
 849        NICK_IN_USE,
 850        PASSWORD_REQUIRED,
 851        BANNED,
 852        MEMBERS_ONLY,
 853        RESOURCE_CONSTRAINT,
 854        KICKED,
 855        SHUTDOWN,
 856        DESTROYED,
 857        INVALID_NICK,
 858        TECHNICAL_PROBLEMS,
 859        UNKNOWN,
 860        NON_ANONYMOUS
 861    }
 862
 863    private interface OnEventListener {
 864        void onSuccess();
 865
 866        void onFailure();
 867    }
 868
 869    public interface OnRenameListener extends OnEventListener {}
 870
 871    public static class Hat implements Comparable<Hat> {
 872        private final Uri uri;
 873        private final String title;
 874
 875        public Hat(final Element el) {
 876            Uri parseUri = null; // null hat uri is invaild per spec
 877            try {
 878                parseUri = Uri.parse(el.getAttribute("uri"));
 879            } catch (final Exception e) { }
 880            uri = parseUri;
 881
 882            title = el.getAttribute("title");
 883        }
 884
 885        public Hat(final Uri uri, final String title) {
 886            this.uri = uri;
 887            this.title = title;
 888        }
 889
 890        public String toString() {
 891            return title == null ? "" : title;
 892        }
 893
 894        public int getColor() {
 895            return UIHelper.getColorForName(uri == null ? toString() : uri.toString());
 896        }
 897
 898        @Override
 899        public int compareTo(@NonNull Hat another) {
 900            return toString().compareTo(another.toString());
 901        }
 902    }
 903
 904    public static class User implements Comparable<User>, AvatarService.Avatarable {
 905        private Role role = Role.NONE;
 906        private Affiliation affiliation = Affiliation.NONE;
 907        private Jid realJid;
 908        private Jid fullJid;
 909        protected String nick;
 910        private long pgpKeyId = 0;
 911        private String avatar;
 912        private final MucOptions options;
 913        private ChatState chatState = Config.DEFAULT_CHAT_STATE;
 914        protected Set<Hat> hats;
 915        protected String occupantId;
 916        protected boolean online = true;
 917
 918        public User(final MucOptions options, Jid fullJid, final String occupantId, final String nick, final Set<Hat> hats) {
 919            this.options = options;
 920            this.fullJid = fullJid;
 921            this.occupantId = occupantId;
 922            this.nick = nick;
 923            this.hats = hats;
 924
 925            if (occupantId != null && options != null) {
 926                avatar = options.getConversation().getAttribute("occupantAvatar/" + occupantId);
 927
 928                if (nick == null) {
 929                    this.nick = options.getConversation().getAttribute("occupantNick/" + occupantId);
 930                } else if (!getNick().equals(getName())) {
 931                    options.getConversation().setAttribute("occupantNick/" + occupantId, nick);
 932                } else {
 933                    options.getConversation().setAttribute("occupantNick/" + occupantId, (String) null);
 934                }
 935            }
 936        }
 937
 938        public String getName() {
 939            return fullJid == null ? null : fullJid.getResource();
 940        }
 941
 942        public Jid getMuc() {
 943            return fullJid == null ? (options.getConversation().getJid().asBareJid()) : fullJid.asBareJid();
 944        }
 945
 946        public String getNick() {
 947            return nick == null ? getName() : nick;
 948        }
 949
 950        public void setOnline(final boolean o) {
 951            online = o;
 952        }
 953
 954        public boolean isOnline() {
 955            return fullJid != null && online;
 956        }
 957
 958        public Role getRole() {
 959            return this.role;
 960        }
 961
 962        public void setRole(final Role role) {
 963            this.role = role;
 964        }
 965
 966        public Affiliation getAffiliation() {
 967            return this.affiliation;
 968        }
 969
 970        public void setAffiliation(final Affiliation affiliation) {
 971            this.affiliation = affiliation;
 972        }
 973
 974        public Set<Hat> getHats() {
 975            return this.hats == null ? new HashSet<>() : hats;
 976        }
 977
 978        public List<MucOptions.Hat> getPseudoHats(Context context) {
 979            List<MucOptions.Hat> hats = new ArrayList<>();
 980            if (getAffiliation() != Affiliation.NONE) {
 981                hats.add(new MucOptions.Hat(null, context.getString(ConferenceDetailsActivity.affiliationToStringRes(getAffiliation()))));
 982            }
 983            if (getRole() != Role.PARTICIPANT) {
 984                hats.add(new MucOptions.Hat(null, context.getString(ConferenceDetailsActivity.roleToStringRes(getRole()))));
 985            }
 986            return hats;
 987        }
 988
 989        public long getPgpKeyId() {
 990            if (this.pgpKeyId != 0) {
 991                return this.pgpKeyId;
 992            } else if (realJid != null) {
 993                return getAccount().getRoster().getContact(realJid).getPgpKeyId();
 994            } else {
 995                return 0;
 996            }
 997        }
 998
 999        public void setPgpKeyId(long id) {
1000            this.pgpKeyId = id;
1001        }
1002
1003        public Contact getContact() {
1004            if (fullJid != null) {
1005                return realJid == null
1006                        ? null
1007                        : getAccount().getRoster().getContactFromContactList(realJid);
1008            } else if (realJid != null) {
1009                return getAccount().getRoster().getContact(realJid);
1010            } else {
1011                return null;
1012            }
1013        }
1014
1015        public boolean setAvatar(final String avatar) {
1016            if (occupantId != null) {
1017                options.getConversation().setAttribute("occupantAvatar/" + occupantId, getContact() == null && avatar != null ? avatar : null);
1018            }
1019            if (this.avatar != null && this.avatar.equals(avatar)) {
1020                return false;
1021            } else {
1022                this.avatar = avatar;
1023                return true;
1024            }
1025        }
1026
1027        public String getAvatar() {
1028
1029            // TODO prefer potentially better quality avatars from contact
1030            // TODO use getContact and if that’s not null and avatar is set use that
1031
1032            getContact();
1033
1034            if (avatar != null) {
1035                return avatar;
1036            }
1037            if (realJid == null) {
1038                return null;
1039            }
1040            final var contact = getAccount().getRoster().getContact(realJid);
1041            return contact.getAvatar();
1042        }
1043
1044        public Cid getAvatarCid() {
1045            final var sha1 = getAvatar();
1046            if (sha1 == null) return null;
1047            try {
1048                return CryptoHelper.cid(CryptoHelper.hexToBytes(sha1), "sha-1");
1049            } catch (NoSuchAlgorithmException e) {
1050                Log.e(Config.LOGTAG, "" + e);
1051                return null;
1052            }
1053        }
1054
1055        public Account getAccount() {
1056            return options.getAccount();
1057        }
1058
1059        public MucOptions getMucOptions() {
1060            return this.options;
1061        }
1062
1063        public Conversation getConversation() {
1064            return options.getConversation();
1065        }
1066
1067        public Jid getFullJid() {
1068            return fullJid;
1069        }
1070
1071        @Override
1072        public boolean equals(Object o) {
1073            if (this == o) return true;
1074            if (o == null || getClass() != o.getClass()) return false;
1075
1076            User user = (User) o;
1077
1078            if (role != user.role) return false;
1079            if (affiliation != user.affiliation) return false;
1080            if (!Objects.equals(realJid, user.realJid)) return false;
1081            return Objects.equals(fullJid, user.fullJid);
1082        }
1083
1084        public boolean isDomain() {
1085            return realJid != null && realJid.getLocal() == null && role == Role.NONE;
1086        }
1087
1088        @Override
1089        public int hashCode() {
1090            int result = role != null ? role.hashCode() : 0;
1091            result = 31 * result + (affiliation != null ? affiliation.hashCode() : 0);
1092            result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
1093            result = 31 * result + (fullJid != null ? fullJid.hashCode() : 0);
1094            return result;
1095        }
1096
1097        @Override
1098        public String toString() {
1099            return "[fulljid:"
1100                    + fullJid
1101                    + ",realjid:"
1102                    + realJid
1103                    + ",affiliation"
1104                    + affiliation.toString()
1105                    + "]";
1106        }
1107
1108        public boolean realJidMatchesAccount() {
1109            return realJid != null && realJid.equals(options.account.getJid().asBareJid());
1110        }
1111
1112        @Override
1113        public int compareTo(@NonNull User another) {
1114            final var anotherPseudoId = another.getOccupantId() != null && another.getOccupantId().charAt(0) == '\0';
1115            final var pseudoId = getOccupantId() != null && getOccupantId().charAt(0) == '\0';
1116            if (anotherPseudoId && !pseudoId) {
1117                return 1;
1118            }
1119            if (pseudoId && !anotherPseudoId) {
1120                return -1;
1121            }
1122            if (another.outranks(getAffiliation())) {
1123                return 1;
1124            } else if (outranks(another.getAffiliation())) {
1125                return -1;
1126            } else {
1127                return getComparableName().compareToIgnoreCase(another.getComparableName());
1128            }
1129        }
1130
1131        public String getComparableName() {
1132            Contact contact = getContact();
1133            if (contact != null) {
1134                return contact.getDisplayName();
1135            } else {
1136                String name = getName();
1137                return name == null ? "" : name;
1138            }
1139        }
1140
1141        public Jid getRealJid() {
1142            return realJid;
1143        }
1144
1145        public void setRealJid(Jid jid) {
1146            this.realJid = jid != null ? jid.asBareJid() : null;
1147        }
1148
1149        public boolean setChatState(ChatState chatState) {
1150            if (this.chatState == chatState) {
1151                return false;
1152            }
1153            this.chatState = chatState;
1154            return true;
1155        }
1156
1157        @Override
1158        public int getAvatarBackgroundColor() {
1159            final String seed = realJid != null ? realJid.asBareJid().toString() : null;
1160            return UIHelper.getColorForName(seed == null ? getName() : seed);
1161        }
1162
1163        @Override
1164        public String getAvatarName() {
1165            return getConversation().getName().toString();
1166        }
1167
1168        public void setOccupantId(final String occupantId) {
1169            this.occupantId = occupantId;
1170        }
1171
1172        public String getOccupantId() {
1173            return this.occupantId;
1174        }
1175
1176        public boolean ranks(final Role role) {
1177            return ROLE_RANKS.getInt(this.role) >= ROLE_RANKS.getInt(role);
1178        }
1179
1180        public boolean ranks(final Affiliation affiliation) {
1181            return AFFILIATION_RANKS.getInt(this.affiliation)
1182                    >= AFFILIATION_RANKS.getInt(affiliation);
1183        }
1184
1185        public boolean outranks(final Affiliation affiliation) {
1186            return AFFILIATION_RANKS.getInt(this.affiliation)
1187                    > AFFILIATION_RANKS.getInt(affiliation);
1188        }
1189    }
1190}