MucOptions.java

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