mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
Show group invite link in group list
This commit is contained in:
parent
6be0b2da77
commit
9912da9546
7 changed files with 212 additions and 3 deletions
|
@ -4,6 +4,7 @@ import net.sourceforge.argparse4j.impl.Arguments;
|
||||||
import net.sourceforge.argparse4j.inf.Namespace;
|
import net.sourceforge.argparse4j.inf.Namespace;
|
||||||
import net.sourceforge.argparse4j.inf.Subparser;
|
import net.sourceforge.argparse4j.inf.Subparser;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.GroupInviteLinkUrl;
|
||||||
import org.asamk.signal.manager.Manager;
|
import org.asamk.signal.manager.Manager;
|
||||||
import org.asamk.signal.storage.groups.GroupInfo;
|
import org.asamk.signal.storage.groups.GroupInfo;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
@ -35,15 +36,18 @@ public class ListGroupsCommand implements LocalCommand {
|
||||||
.map(SignalServiceAddress::getLegacyIdentifier)
|
.map(SignalServiceAddress::getLegacyIdentifier)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
final GroupInviteLinkUrl groupInviteLink = group.getGroupInviteLink();
|
||||||
|
|
||||||
System.out.println(String.format(
|
System.out.println(String.format(
|
||||||
"Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s",
|
"Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s Link: %s",
|
||||||
Base64.encodeBytes(group.groupId),
|
Base64.encodeBytes(group.groupId),
|
||||||
group.getTitle(),
|
group.getTitle(),
|
||||||
group.isMember(m.getSelfAddress()),
|
group.isMember(m.getSelfAddress()),
|
||||||
group.isBlocked(),
|
group.isBlocked(),
|
||||||
members,
|
members,
|
||||||
pendingMembers,
|
pendingMembers,
|
||||||
requestingMembers));
|
requestingMembers,
|
||||||
|
groupInviteLink == null ? '-' : groupInviteLink.getUrl()));
|
||||||
} else {
|
} else {
|
||||||
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b",
|
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b",
|
||||||
Base64.encodeBytes(group.groupId),
|
Base64.encodeBytes(group.groupId),
|
||||||
|
|
140
src/main/java/org/asamk/signal/manager/GroupInviteLinkUrl.java
Normal file
140
src/main/java/org/asamk/signal/manager/GroupInviteLinkUrl.java
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
package org.asamk.signal.manager;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import org.signal.storageservice.protos.groups.GroupInviteLink;
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
|
import org.whispersystems.util.Base64UrlSafe;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
public final class GroupInviteLinkUrl {
|
||||||
|
|
||||||
|
private static final String GROUP_URL_HOST = "signal.group";
|
||||||
|
private static final String GROUP_URL_PREFIX = "https://" + GROUP_URL_HOST + "/#";
|
||||||
|
|
||||||
|
private final GroupMasterKey groupMasterKey;
|
||||||
|
private final GroupLinkPassword password;
|
||||||
|
private final String url;
|
||||||
|
|
||||||
|
public static GroupInviteLinkUrl forGroup(GroupMasterKey groupMasterKey, DecryptedGroup group) {
|
||||||
|
return new GroupInviteLinkUrl(groupMasterKey,
|
||||||
|
GroupLinkPassword.fromBytes(group.getInviteLinkPassword().toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isGroupLink(String urlString) {
|
||||||
|
return getGroupUrl(urlString) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return null iff not a group url.
|
||||||
|
* @throws InvalidGroupLinkException If group url, but cannot be parsed.
|
||||||
|
*/
|
||||||
|
public static GroupInviteLinkUrl fromUri(String urlString) throws InvalidGroupLinkException, UnknownGroupLinkVersionException {
|
||||||
|
URI uri = getGroupUrl(urlString);
|
||||||
|
|
||||||
|
if (uri == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!"/".equals(uri.getPath()) && uri.getPath().length() > 0) {
|
||||||
|
throw new InvalidGroupLinkException("No path was expected in uri");
|
||||||
|
}
|
||||||
|
|
||||||
|
String encoding = uri.getFragment();
|
||||||
|
|
||||||
|
if (encoding == null || encoding.length() == 0) {
|
||||||
|
throw new InvalidGroupLinkException("No reference was in the uri");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = Base64UrlSafe.decodePaddingAgnostic(encoding);
|
||||||
|
GroupInviteLink groupInviteLink = GroupInviteLink.parseFrom(bytes);
|
||||||
|
|
||||||
|
switch (groupInviteLink.getContentsCase()) {
|
||||||
|
case V1CONTENTS: {
|
||||||
|
GroupInviteLink.GroupInviteLinkContentsV1 groupInviteLinkContentsV1 = groupInviteLink.getV1Contents();
|
||||||
|
GroupMasterKey groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.getGroupMasterKey()
|
||||||
|
.toByteArray());
|
||||||
|
GroupLinkPassword password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.getInviteLinkPassword()
|
||||||
|
.toByteArray());
|
||||||
|
|
||||||
|
return new GroupInviteLinkUrl(groupMasterKey, password);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new UnknownGroupLinkVersionException("Url contains no known group link content");
|
||||||
|
}
|
||||||
|
} catch (InvalidInputException | IOException e) {
|
||||||
|
throw new InvalidGroupLinkException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@link URI} if the host name matches.
|
||||||
|
*/
|
||||||
|
private static URI getGroupUrl(String urlString) {
|
||||||
|
try {
|
||||||
|
URI url = new URI(urlString);
|
||||||
|
|
||||||
|
if (!"https".equalsIgnoreCase(url.getScheme()) && !"sgnl".equalsIgnoreCase(url.getScheme())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GROUP_URL_HOST.equalsIgnoreCase(url.getHost()) ? url : null;
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupInviteLinkUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
|
||||||
|
this.groupMasterKey = groupMasterKey;
|
||||||
|
this.password = password;
|
||||||
|
this.url = createUrl(groupMasterKey, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
|
||||||
|
GroupInviteLink groupInviteLink = GroupInviteLink.newBuilder()
|
||||||
|
.setV1Contents(GroupInviteLink.GroupInviteLinkContentsV1.newBuilder()
|
||||||
|
.setGroupMasterKey(ByteString.copyFrom(groupMasterKey.serialize()))
|
||||||
|
.setInviteLinkPassword(ByteString.copyFrom(password.serialize())))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String encoding = Base64UrlSafe.encodeBytesWithoutPadding(groupInviteLink.toByteArray());
|
||||||
|
|
||||||
|
return GROUP_URL_PREFIX + encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GroupMasterKey getGroupMasterKey() {
|
||||||
|
return groupMasterKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GroupLinkPassword getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final static class InvalidGroupLinkException extends Exception {
|
||||||
|
|
||||||
|
public InvalidGroupLinkException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidGroupLinkException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final static class UnknownGroupLinkVersionException extends Exception {
|
||||||
|
|
||||||
|
public UnknownGroupLinkVersionException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package org.asamk.signal.manager;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public final class GroupLinkPassword {
|
||||||
|
|
||||||
|
private static final int SIZE = 16;
|
||||||
|
|
||||||
|
private final byte[] bytes;
|
||||||
|
|
||||||
|
public static GroupLinkPassword createNew() {
|
||||||
|
return new GroupLinkPassword(KeyUtils.getSecretBytes(SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GroupLinkPassword fromBytes(byte[] bytes) {
|
||||||
|
return new GroupLinkPassword(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupLinkPassword(byte[] bytes) {
|
||||||
|
this.bytes = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] serialize() {
|
||||||
|
return bytes.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (!(other instanceof GroupLinkPassword)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.equals(bytes, ((GroupLinkPassword) other).bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Arrays.hashCode(bytes);
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,7 +39,7 @@ class KeyUtils {
|
||||||
return Base64.encodeBytes(secret);
|
return Base64.encodeBytes(secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] getSecretBytes(int size) {
|
static byte[] getSecretBytes(int size) {
|
||||||
byte[] secret = new byte[size];
|
byte[] secret = new byte[size];
|
||||||
RandomUtils.getSecureRandom().nextBytes(secret);
|
RandomUtils.getSecureRandom().nextBytes(secret);
|
||||||
return secret;
|
return secret;
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.asamk.signal.storage.groups;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.GroupInviteLinkUrl;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -21,6 +22,9 @@ public abstract class GroupInfo {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public abstract String getTitle();
|
public abstract String getTitle();
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public abstract GroupInviteLinkUrl getGroupInviteLink();
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public abstract Set<SignalServiceAddress> getMembers();
|
public abstract Set<SignalServiceAddress> getMembers();
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.GroupInviteLinkUrl;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -55,6 +56,11 @@ public class GroupInfoV1 extends GroupInfo {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GroupInviteLinkUrl getGroupInviteLink() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public GroupInfoV1(
|
public GroupInfoV1(
|
||||||
@JsonProperty("groupId") byte[] groupId,
|
@JsonProperty("groupId") byte[] groupId,
|
||||||
@JsonProperty("expectedV2Id") byte[] expectedV2Id,
|
@JsonProperty("expectedV2Id") byte[] expectedV2Id,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package org.asamk.signal.storage.groups;
|
package org.asamk.signal.storage.groups;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.GroupInviteLinkUrl;
|
||||||
|
import org.signal.storageservice.protos.groups.AccessControl;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
@ -41,6 +43,19 @@ public class GroupInfoV2 extends GroupInfo {
|
||||||
return this.group.getTitle();
|
return this.group.getTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GroupInviteLinkUrl getGroupInviteLink() {
|
||||||
|
if (this.group == null || this.group.getInviteLinkPassword() == null || (
|
||||||
|
this.group.getAccessControl().getAddFromInviteLink() != AccessControl.AccessRequired.ANY
|
||||||
|
&& this.group.getAccessControl().getAddFromInviteLink()
|
||||||
|
!= AccessControl.AccessRequired.ADMINISTRATOR
|
||||||
|
)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GroupInviteLinkUrl.forGroup(masterKey, group);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<SignalServiceAddress> getMembers() {
|
public Set<SignalServiceAddress> getMembers() {
|
||||||
if (this.group == null) {
|
if (this.group == null) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue