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