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