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