MucOptions.java

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