mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-04-30 06:34:28 +02:00
fix(YouTube - Spoof video streams): Update client user-agent (#4304)
This commit is contained in:
parent
e89fd80ec9
commit
7917871f51
@ -4,6 +4,10 @@ import android.os.Build;
|
|||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
|
||||||
public enum ClientType {
|
public enum ClientType {
|
||||||
@ -11,13 +15,17 @@ public enum ClientType {
|
|||||||
ANDROID_VR_NO_AUTH(
|
ANDROID_VR_NO_AUTH(
|
||||||
28,
|
28,
|
||||||
"ANDROID_VR",
|
"ANDROID_VR",
|
||||||
|
"com.google.android.apps.youtube.vr.oculus",
|
||||||
"Oculus",
|
"Oculus",
|
||||||
"Quest 3",
|
"Quest 3",
|
||||||
"Android",
|
"Android",
|
||||||
"12",
|
"12",
|
||||||
"com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip",
|
// Android 12.1
|
||||||
"32", // Android 12.1
|
"32",
|
||||||
"1.56.21",
|
"SQ3A.220605.009.A1",
|
||||||
|
"132.0.6808.3",
|
||||||
|
"1.61.48",
|
||||||
|
false,
|
||||||
false,
|
false,
|
||||||
"Android VR No auth"
|
"Android VR No auth"
|
||||||
),
|
),
|
||||||
@ -26,67 +34,81 @@ public enum ClientType {
|
|||||||
ANDROID_UNPLUGGED(
|
ANDROID_UNPLUGGED(
|
||||||
29,
|
29,
|
||||||
"ANDROID_UNPLUGGED",
|
"ANDROID_UNPLUGGED",
|
||||||
|
"com.google.android.apps.youtube.unplugged",
|
||||||
"Google",
|
"Google",
|
||||||
"Google TV Streamer",
|
"Google TV Streamer",
|
||||||
"Android",
|
"Android",
|
||||||
"14",
|
"14",
|
||||||
"com.google.android.apps.youtube.unplugged/8.49.0 (Linux; U; Android 14; GB) gzip",
|
|
||||||
"34",
|
"34",
|
||||||
|
"UTT3.240625.001.K5",
|
||||||
|
"132.0.6808.3",
|
||||||
"8.49.0",
|
"8.49.0",
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
"Android TV"
|
"Android TV"
|
||||||
),
|
),
|
||||||
// Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
|
// Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
|
||||||
|
// Google Pixel 9 Pro Fold
|
||||||
|
// https://dumps.tadiphone.dev/dumps/google/barbet
|
||||||
ANDROID_CREATOR(
|
ANDROID_CREATOR(
|
||||||
14,
|
14,
|
||||||
"ANDROID_CREATOR",
|
"ANDROID_CREATOR",
|
||||||
Build.MANUFACTURER,
|
"com.google.android.apps.youtube.creator",
|
||||||
Build.MODEL,
|
"Google",
|
||||||
|
"Pixel 9 Pro Fold",
|
||||||
"Android",
|
"Android",
|
||||||
"11",
|
"15",
|
||||||
"com.google.android.apps.youtube.creator/24.45.100 (Linux; U; Android 11) gzip",
|
"35",
|
||||||
"30",
|
"AP3A.241005.015.A2",
|
||||||
"24.45.100",
|
"132.0.6779.0",
|
||||||
|
"23.47.101",
|
||||||
|
true,
|
||||||
true,
|
true,
|
||||||
"Android Creator"
|
"Android Creator"
|
||||||
),
|
),
|
||||||
ANDROID_VR(
|
ANDROID_VR(
|
||||||
ANDROID_VR_NO_AUTH.id,
|
ANDROID_VR_NO_AUTH.id,
|
||||||
ANDROID_VR_NO_AUTH.clientName,
|
ANDROID_VR_NO_AUTH.clientName,
|
||||||
|
ANDROID_VR_NO_AUTH.packageName,
|
||||||
ANDROID_VR_NO_AUTH.deviceMake,
|
ANDROID_VR_NO_AUTH.deviceMake,
|
||||||
ANDROID_VR_NO_AUTH.deviceModel,
|
ANDROID_VR_NO_AUTH.deviceModel,
|
||||||
ANDROID_VR_NO_AUTH.osName,
|
ANDROID_VR_NO_AUTH.osName,
|
||||||
ANDROID_VR_NO_AUTH.osVersion,
|
ANDROID_VR_NO_AUTH.osVersion,
|
||||||
ANDROID_VR_NO_AUTH.userAgent,
|
|
||||||
ANDROID_VR_NO_AUTH.androidSdkVersion,
|
ANDROID_VR_NO_AUTH.androidSdkVersion,
|
||||||
|
ANDROID_VR_NO_AUTH.buildId,
|
||||||
|
ANDROID_VR_NO_AUTH.cronetVersion,
|
||||||
ANDROID_VR_NO_AUTH.clientVersion,
|
ANDROID_VR_NO_AUTH.clientVersion,
|
||||||
|
ANDROID_VR_NO_AUTH.requiresAuth,
|
||||||
true,
|
true,
|
||||||
"Android VR"
|
"Android VR"
|
||||||
),
|
),
|
||||||
IOS_UNPLUGGED(
|
IOS_UNPLUGGED(
|
||||||
33,
|
33,
|
||||||
"IOS_UNPLUGGED",
|
"IOS_UNPLUGGED",
|
||||||
|
"com.google.ios.youtubeunplugged",
|
||||||
"Apple",
|
"Apple",
|
||||||
forceAVC()
|
forceAVC()
|
||||||
? "iPhone12,5" // 11 Pro Max (last device with iOS 13)
|
// 11 Pro Max (last device with iOS 13)
|
||||||
: "iPhone16,2", // 15 Pro Max
|
? "iPhone12,5"
|
||||||
|
// 15 Pro Max
|
||||||
|
: "iPhone16,2",
|
||||||
"iOS",
|
"iOS",
|
||||||
|
forceAVC()
|
||||||
// iOS 13 and earlier uses only AVC. 14+ adds VP9 and AV1.
|
// iOS 13 and earlier uses only AVC. 14+ adds VP9 and AV1.
|
||||||
forceAVC()
|
? "13.7.17H35"
|
||||||
? "13.7.17H35" // Last release of iOS 13.
|
|
||||||
: "18.2.22C152",
|
: "18.2.22C152",
|
||||||
forceAVC()
|
null,
|
||||||
? "com.google.ios.youtubeunplugged/6.45 (iPhone12,5; U; CPU iOS 13_7 like Mac OS X)"
|
null,
|
||||||
: "com.google.ios.youtubeunplugged/8.49 (iPhone16,2; U; CPU iOS 18_2_22 like Mac OS X)",
|
|
||||||
null,
|
null,
|
||||||
// Version number should be a valid iOS release.
|
// Version number should be a valid iOS release.
|
||||||
// https://www.ipa4fun.com/history/152043/
|
// https://www.ipa4fun.com/history/152043/
|
||||||
|
forceAVC()
|
||||||
// Some newer versions can also force AVC,
|
// Some newer versions can also force AVC,
|
||||||
// but 6.45 is the last version that supports iOS 13.
|
// but 6.45 is the last version that supports iOS 13.
|
||||||
forceAVC()
|
|
||||||
? "6.45"
|
? "6.45"
|
||||||
: "8.49",
|
: "8.49",
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
forceAVC()
|
forceAVC()
|
||||||
? "iOS TV Force AVC"
|
? "iOS TV Force AVC"
|
||||||
: "iOS TV"
|
: "iOS TV"
|
||||||
@ -104,6 +126,16 @@ public enum ClientType {
|
|||||||
|
|
||||||
public final String clientName;
|
public final String clientName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App package name.
|
||||||
|
*/
|
||||||
|
private final String packageName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Player user-agent.
|
||||||
|
*/
|
||||||
|
public final String userAgent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Device model, equivalent to {@link Build#MANUFACTURER} (System property: ro.product.vendor.manufacturer)
|
* Device model, equivalent to {@link Build#MANUFACTURER} (System property: ro.product.vendor.manufacturer)
|
||||||
*/
|
*/
|
||||||
@ -124,11 +156,6 @@ public enum ClientType {
|
|||||||
*/
|
*/
|
||||||
public final String osVersion;
|
public final String osVersion;
|
||||||
|
|
||||||
/**
|
|
||||||
* Player user-agent.
|
|
||||||
*/
|
|
||||||
public final String userAgent;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
|
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
|
||||||
* Field is null if not applicable.
|
* Field is null if not applicable.
|
||||||
@ -136,43 +163,97 @@ public enum ClientType {
|
|||||||
@Nullable
|
@Nullable
|
||||||
public final String androidSdkVersion;
|
public final String androidSdkVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android build id, equivalent to {@link Build#ID}.
|
||||||
|
* Field is null if not applicable.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private final String buildId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cronet release version, as found in decompiled client apk.
|
||||||
|
* Field is null if not applicable.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private final String cronetVersion;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App version.
|
* App version.
|
||||||
*/
|
*/
|
||||||
public final String clientVersion;
|
public final String clientVersion;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the client can access the API logged in.
|
* If this client requires authentication and does not work
|
||||||
|
* if logged out or in incognito mode.
|
||||||
*/
|
*/
|
||||||
public final boolean canLogin;
|
public final boolean requiresAuth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the client should use authentication if available.
|
||||||
|
*/
|
||||||
|
public final boolean useAuth;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Friendly name displayed in stats for nerds.
|
* Friendly name displayed in stats for nerds.
|
||||||
*/
|
*/
|
||||||
public final String friendlyName;
|
public final String friendlyName;
|
||||||
|
|
||||||
|
@SuppressWarnings("ConstantLocale")
|
||||||
ClientType(int id,
|
ClientType(int id,
|
||||||
String clientName,
|
String clientName,
|
||||||
|
String packageName,
|
||||||
String deviceMake,
|
String deviceMake,
|
||||||
String deviceModel,
|
String deviceModel,
|
||||||
String osName,
|
String osName,
|
||||||
String osVersion,
|
String osVersion,
|
||||||
String userAgent,
|
|
||||||
@Nullable String androidSdkVersion,
|
@Nullable String androidSdkVersion,
|
||||||
|
@Nullable String buildId,
|
||||||
|
@Nullable String cronetVersion,
|
||||||
String clientVersion,
|
String clientVersion,
|
||||||
boolean canLogin,
|
boolean requiresAuth,
|
||||||
|
boolean useAuth,
|
||||||
String friendlyName) {
|
String friendlyName) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.clientName = clientName;
|
this.clientName = clientName;
|
||||||
|
this.packageName = packageName;
|
||||||
this.deviceMake = deviceMake;
|
this.deviceMake = deviceMake;
|
||||||
this.deviceModel = deviceModel;
|
this.deviceModel = deviceModel;
|
||||||
this.osName = osName;
|
this.osName = osName;
|
||||||
this.osVersion = osVersion;
|
this.osVersion = osVersion;
|
||||||
this.userAgent = userAgent;
|
|
||||||
this.androidSdkVersion = androidSdkVersion;
|
this.androidSdkVersion = androidSdkVersion;
|
||||||
|
this.buildId = buildId;
|
||||||
|
this.cronetVersion = cronetVersion;
|
||||||
this.clientVersion = clientVersion;
|
this.clientVersion = clientVersion;
|
||||||
this.canLogin = canLogin;
|
this.requiresAuth = requiresAuth;
|
||||||
|
this.useAuth = useAuth;
|
||||||
this.friendlyName = friendlyName;
|
this.friendlyName = friendlyName;
|
||||||
|
|
||||||
|
Locale defaultLocale = Locale.getDefault();
|
||||||
|
if (androidSdkVersion == null) {
|
||||||
|
// Convert version from '18.2.22C152' into '18_2_22'
|
||||||
|
String userAgentOsVersion = osVersion
|
||||||
|
.replaceAll("(\\d+\\.\\d+\\.\\d+).*", "$1")
|
||||||
|
.replace(".", "_");
|
||||||
|
// https://github.com/mitmproxy/mitmproxy/issues/4836
|
||||||
|
this.userAgent = String.format("%s/%s (%s; U; CPU iOS %s like Mac OS X; %s)",
|
||||||
|
packageName,
|
||||||
|
clientVersion,
|
||||||
|
deviceModel,
|
||||||
|
userAgentOsVersion,
|
||||||
|
defaultLocale
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s; Cronet/%s)",
|
||||||
|
packageName,
|
||||||
|
clientVersion,
|
||||||
|
osVersion,
|
||||||
|
defaultLocale,
|
||||||
|
deviceModel,
|
||||||
|
Objects.requireNonNull(buildId),
|
||||||
|
Objects.requireNonNull(cronetVersion)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Logger.printDebug(() -> "userAgent: " + this.userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,12 @@ import org.json.JSONObject;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.requests.Requester;
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
import app.revanced.extension.shared.requests.Route;
|
import app.revanced.extension.shared.requests.Route;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.AppLanguage;
|
|
||||||
import app.revanced.extension.shared.spoof.ClientType;
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
|
|
||||||
final class PlayerRoutes {
|
final class PlayerRoutes {
|
||||||
@ -31,7 +31,7 @@ final class PlayerRoutes {
|
|||||||
private PlayerRoutes() {
|
private PlayerRoutes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static String createInnertubeBody(ClientType clientType) {
|
static String createInnertubeBody(ClientType clientType, String videoId) {
|
||||||
JSONObject innerTubeBody = new JSONObject();
|
JSONObject innerTubeBody = new JSONObject();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -42,27 +42,28 @@ final class PlayerRoutes {
|
|||||||
// but if this is a fall over client it will set the language even though
|
// but if this is a fall over client it will set the language even though
|
||||||
// the audio language is not selectable in the UI.
|
// the audio language is not selectable in the UI.
|
||||||
ClientType userSelectedClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
ClientType userSelectedClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
||||||
AppLanguage language = userSelectedClient == ClientType.ANDROID_VR_NO_AUTH
|
Locale streamLocale = userSelectedClient == ClientType.ANDROID_VR_NO_AUTH
|
||||||
? BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get()
|
? BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get().getLocale()
|
||||||
: AppLanguage.DEFAULT;
|
: Locale.getDefault();
|
||||||
|
|
||||||
JSONObject client = new JSONObject();
|
JSONObject client = new JSONObject();
|
||||||
client.put("hl", language.getLanguage());
|
|
||||||
client.put("clientName", clientType.clientName);
|
|
||||||
client.put("clientVersion", clientType.clientVersion);
|
|
||||||
client.put("deviceMake", clientType.deviceMake);
|
client.put("deviceMake", clientType.deviceMake);
|
||||||
client.put("deviceModel", clientType.deviceModel);
|
client.put("deviceModel", clientType.deviceModel);
|
||||||
|
client.put("clientName", clientType.clientName);
|
||||||
|
client.put("clientVersion", clientType.clientVersion);
|
||||||
client.put("osName", clientType.osName);
|
client.put("osName", clientType.osName);
|
||||||
client.put("osVersion", clientType.osVersion);
|
client.put("osVersion", clientType.osVersion);
|
||||||
if (clientType.androidSdkVersion != null) {
|
if (clientType.androidSdkVersion != null) {
|
||||||
client.put("androidSdkVersion", clientType.androidSdkVersion);
|
client.put("androidSdkVersion", clientType.androidSdkVersion);
|
||||||
}
|
}
|
||||||
|
client.put("hl", streamLocale.getLanguage());
|
||||||
|
client.put("gl", streamLocale.getCountry());
|
||||||
context.put("client", client);
|
context.put("client", client);
|
||||||
|
|
||||||
innerTubeBody.put("context", context);
|
innerTubeBody.put("context", context);
|
||||||
innerTubeBody.put("contentCheckOk", true);
|
innerTubeBody.put("contentCheckOk", true);
|
||||||
innerTubeBody.put("racyCheckOk", true);
|
innerTubeBody.put("racyCheckOk", true);
|
||||||
innerTubeBody.put("videoId", "%s");
|
innerTubeBody.put("videoId", videoId);
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
Logger.printException(() -> "Failed to create innerTubeBody", e);
|
Logger.printException(() -> "Failed to create innerTubeBody", e);
|
||||||
}
|
}
|
||||||
@ -78,7 +79,9 @@ final class PlayerRoutes {
|
|||||||
|
|
||||||
connection.setRequestProperty("Content-Type", "application/json");
|
connection.setRequestProperty("Content-Type", "application/json");
|
||||||
connection.setRequestProperty("User-Agent", clientType.userAgent);
|
connection.setRequestProperty("User-Agent", clientType.userAgent);
|
||||||
connection.setRequestProperty("X-YouTube-Client-Version", String.valueOf(clientType.id));
|
// Not a typo. "Client-Name" uses the client type id.
|
||||||
|
connection.setRequestProperty("X-YouTube-Client-Name", String.valueOf(clientType.id));
|
||||||
|
connection.setRequestProperty("X-YouTube-Client-Version", clientType.clientVersion);
|
||||||
|
|
||||||
connection.setUseCaches(false);
|
connection.setUseCaches(false);
|
||||||
connection.setDoOutput(true);
|
connection.setDoOutput(true);
|
||||||
|
@ -120,7 +120,8 @@ public class StreamingDataRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static HttpURLConnection send(ClientType clientType, String videoId,
|
private static HttpURLConnection send(ClientType clientType,
|
||||||
|
String videoId,
|
||||||
Map<String, String> playerHeaders,
|
Map<String, String> playerHeaders,
|
||||||
boolean showErrorToasts) {
|
boolean showErrorToasts) {
|
||||||
Objects.requireNonNull(clientType);
|
Objects.requireNonNull(clientType);
|
||||||
@ -128,21 +129,24 @@ public class StreamingDataRequest {
|
|||||||
Objects.requireNonNull(playerHeaders);
|
Objects.requireNonNull(playerHeaders);
|
||||||
|
|
||||||
final long startTime = System.currentTimeMillis();
|
final long startTime = System.currentTimeMillis();
|
||||||
Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
|
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
|
||||||
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
||||||
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
||||||
|
|
||||||
|
boolean authHeadersIncludes = false;
|
||||||
|
|
||||||
for (String key : REQUEST_HEADER_KEYS) {
|
for (String key : REQUEST_HEADER_KEYS) {
|
||||||
String value = playerHeaders.get(key);
|
String value = playerHeaders.get(key);
|
||||||
|
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
if (key.equals(AUTHORIZATION_HEADER)) {
|
if (key.equals(AUTHORIZATION_HEADER)) {
|
||||||
if (!clientType.canLogin) {
|
if (!clientType.useAuth) {
|
||||||
Logger.printDebug(() -> "Not including request header: " + key);
|
Logger.printDebug(() -> "Not including request header: " + key);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
authHeadersIncludes = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.printDebug(() -> "Including request header: " + key);
|
Logger.printDebug(() -> "Including request header: " + key);
|
||||||
@ -150,7 +154,15 @@ public class StreamingDataRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId);
|
if (!authHeadersIncludes && clientType.requiresAuth) {
|
||||||
|
Logger.printDebug(() -> "Skipping client since user is not logged in: " + clientType
|
||||||
|
+ " videoId: " + videoId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType);
|
||||||
|
|
||||||
|
String innerTubeBody = PlayerRoutes.createInnertubeBody(clientType, videoId);
|
||||||
byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
|
byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
|
||||||
connection.setFixedLengthStreamingMode(requestBody.length);
|
connection.setFixedLengthStreamingMode(requestBody.length);
|
||||||
connection.getOutputStream().write(requestBody);
|
connection.getOutputStream().write(requestBody);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user