update
This commit is contained in:
@@ -3,9 +3,11 @@ package com.yutou.biliapi.api;
|
||||
import com.yutou.biliapi.bean.login.CheckCookieBean;
|
||||
import com.yutou.biliapi.bean.login.LoginInfoBean;
|
||||
import com.yutou.biliapi.bean.login.QRCodeGenerateBean;
|
||||
import com.yutou.common.okhttp.FileBody;
|
||||
import com.yutou.common.okhttp.HttpBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
public interface LoginApi {
|
||||
@@ -27,5 +29,13 @@ public interface LoginApi {
|
||||
@GET("x/passport-login/web/cookie/info")
|
||||
Call<HttpBody<CheckCookieBean>> checkCookie();
|
||||
|
||||
@GET("https://www.bilibili.com/correspond/1/{correspondPath}")
|
||||
Call<FileBody<String>> getCorrespond(@Path("correspondPath") String correspondPath);
|
||||
|
||||
@GET("/x/passport-login/web/cookie/refresh")
|
||||
Call<HttpBody<LoginInfoBean>> refreshCookie(@Query("csrf") String csrf
|
||||
, @Query("refresh_csrf") String refresh_csrf
|
||||
, @Query("source") String source
|
||||
, @Query("refresh_token") String ac_time_value
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ public class LoginCookieDatabaseBean extends AbsDatabasesBean {
|
||||
String sid;
|
||||
@JSONField(name = "gourl")
|
||||
String gourl;
|
||||
@JSONField(name ="refresh_token")
|
||||
String refreshToken;
|
||||
|
||||
public LoginCookieDatabaseBean() {
|
||||
super("login_cookie", System.currentTimeMillis());
|
||||
|
||||
@@ -54,6 +54,9 @@ public class BiliBiliLoginDatabase extends SQLiteManager {
|
||||
|
||||
return null;
|
||||
}
|
||||
public List<LoginCookieDatabaseBean> getAllCookies(){
|
||||
return super.get(cookie.getTableName(), LoginCookieDatabaseBean.class);
|
||||
}
|
||||
|
||||
public LoginUserDatabaseBean getUser(String userId) {
|
||||
List<LoginUserDatabaseBean> list = getAllUser();
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
package com.yutou.biliapi.net;
|
||||
|
||||
import com.yutou.biliapi.bean.login.CheckCookieBean;
|
||||
import com.yutou.common.inter.IHttpApiCheckCallback;
|
||||
import com.yutou.common.okhttp.HttpCallback;
|
||||
import com.yutou.biliapi.api.LoginApi;
|
||||
import com.yutou.biliapi.bean.login.LoginCookieDatabaseBean;
|
||||
import com.yutou.common.okhttp.FileBody;
|
||||
import com.yutou.common.utils.Log;
|
||||
import com.yutou.common.utils.RSAUtils;
|
||||
import okhttp3.Headers;
|
||||
import com.yutou.common.utils.WebClient;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import retrofit2.Response;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.OAEPParameterSpec;
|
||||
import javax.crypto.spec.PSource;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
@@ -19,8 +26,9 @@ import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
|
||||
public class BiliCookieManager {
|
||||
public static final int COOKIE_INVALID = -101;
|
||||
public static final int COOKIE_SUCCESS = 0;
|
||||
public static final int COOKIE_INVALID = -101;
|
||||
public static final int COOKIE_SUCCESS = 0;
|
||||
|
||||
|
||||
private static final String PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n" +
|
||||
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDLgd2OAkcGVtoE3ThUREbio0Eg\n" +
|
||||
@@ -29,62 +37,54 @@ public class BiliCookieManager {
|
||||
"JNrRuoEUXpabUzGB8QIDAQAB\n" +
|
||||
"-----END PUBLIC KEY-----";
|
||||
|
||||
public void checkCookie(IHttpApiCheckCallback<Integer> callback){
|
||||
BiliLoginNetApiManager.getInstance().getLoginApi(null)
|
||||
.checkCookie().enqueue(new HttpCallback<CheckCookieBean>() {
|
||||
@Override
|
||||
public void onResponse(Headers headers, int code, String status, CheckCookieBean response, String rawResponse) {
|
||||
if(code==-101){
|
||||
// TODO cookie失效,需要重新登录
|
||||
callback.onError(COOKIE_INVALID,"cookie失效,需要重新登录");
|
||||
return;
|
||||
}
|
||||
if(response.isRefresh()){
|
||||
refreshCookie();
|
||||
}
|
||||
callback.onSuccess(COOKIE_SUCCESS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* <a href="https://socialsisteryi.github.io/bilibili-API-collect/docs/login/cookie_refresh.html#java">文档地址</a>
|
||||
*/
|
||||
private void refreshCookie(){
|
||||
public static String getCorrespondPath(String plaintext) throws Exception {
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
String publicKeyStr = PUBLIC_KEY
|
||||
.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "")
|
||||
.replace("\n", "")
|
||||
.trim();
|
||||
byte[] publicBytes = Base64.getDecoder().decode(publicKeyStr);
|
||||
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicBytes);
|
||||
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
|
||||
|
||||
String algorithm = "RSA/ECB/OAEPPadding";
|
||||
Cipher cipher = Cipher.getInstance(algorithm);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
|
||||
// Encode the plaintext to bytes
|
||||
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// Add OAEP padding to the plaintext bytes
|
||||
OAEPParameterSpec oaepParams = new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey, oaepParams);
|
||||
// Encrypt the padded plaintext bytes
|
||||
byte[] encryptedBytes = cipher.doFinal(plaintextBytes);
|
||||
// Convert the encrypted bytes to a Base64-encoded string
|
||||
return new BigInteger(1, encryptedBytes).toString(16);
|
||||
}
|
||||
|
||||
private static String getRefreshCsrf(String htmlContent) {
|
||||
htmlContent=htmlContent.split("<div id=\"1-name\">")[1].split("</div>")[0];
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
public static void resetCookie(LoginCookieDatabaseBean cookie) {
|
||||
try {
|
||||
String refreshTime = String.format("refresh_%d", System.currentTimeMillis());
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
String publicKeyStr = PUBLIC_KEY
|
||||
.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "")
|
||||
.replace("\n", "")
|
||||
.trim();
|
||||
byte[] publicBytes = Base64.getDecoder().decode(publicKeyStr);
|
||||
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicBytes);
|
||||
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
|
||||
LoginApi api = BiliLoginNetApiManager.getInstance().getLoginApi(cookie.getDedeUserID());
|
||||
String correspondPath = getCorrespondPath(String.format("refresh_%d", System.currentTimeMillis()));
|
||||
System.out.println("correspondPath = " + correspondPath);
|
||||
Response<FileBody<String>> body = api.getCorrespond(correspondPath).execute();
|
||||
String string = IOUtils.toString(body.body().getInputStream(), StandardCharsets.UTF_8);
|
||||
String refreshCsrf = getRefreshCsrf(string);
|
||||
System.out.println("body = " + refreshCsrf);
|
||||
|
||||
String algorithm = "RSA/ECB/OAEPPadding";
|
||||
Cipher cipher = Cipher.getInstance(algorithm);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
|
||||
// Encode the plaintext to bytes
|
||||
byte[] plaintextBytes = refreshTime.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// Add OAEP padding to the plaintext bytes
|
||||
OAEPParameterSpec oaepParams = new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey, oaepParams);
|
||||
// Encrypt the padded plaintext bytes
|
||||
byte[] encryptedBytes = cipher.doFinal(plaintextBytes);
|
||||
// Convert the encrypted bytes to a Base64-encoded string
|
||||
String encrypted = new BigInteger(1, encryptedBytes).toString(16);
|
||||
}catch (Exception e){
|
||||
Log.e(e);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ public class BiliLoginNetApiManager extends BaseApi {
|
||||
if (!list.isEmpty()) {
|
||||
ck.put("gourl", bd);
|
||||
LoginCookieDatabaseBean cookie = JSONObject.parseObject(ck.toString(), LoginCookieDatabaseBean.class);
|
||||
cookie.setRefreshToken(httpBody.getData().getRefresh_token());
|
||||
cancel();
|
||||
callback.onResponse(headers, LOGIN_SUCCESS, "ok", cookie, ck.toString());
|
||||
|
||||
|
||||
@@ -2,16 +2,37 @@ package com.yutou.bilibili.Controllers;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.yutou.biliapi.api.LoginApi;
|
||||
import com.yutou.biliapi.bean.login.CheckCookieBean;
|
||||
import com.yutou.biliapi.bean.login.LoginCookieDatabaseBean;
|
||||
import com.yutou.biliapi.bean.login.LoginUserDatabaseBean;
|
||||
import com.yutou.biliapi.bean.login.UserInfoBean;
|
||||
import com.yutou.biliapi.databases.BiliBiliLoginDatabase;
|
||||
import com.yutou.biliapi.net.BiliCookieManager;
|
||||
import com.yutou.biliapi.net.BiliLoginNetApiManager;
|
||||
import com.yutou.biliapi.net.BiliUserNetApiManager;
|
||||
import com.yutou.bilibili.datas.ResultData;
|
||||
import com.yutou.bilibili.services.LiveLoginService;
|
||||
import com.yutou.common.okhttp.HttpBody;
|
||||
import com.yutou.common.okhttp.HttpCallback;
|
||||
import com.yutou.common.okhttp.HttpLoggingInterceptor;
|
||||
import com.yutou.common.utils.Log;
|
||||
import com.yutou.common.utils.RedisTools;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import okhttp3.Headers;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import retrofit2.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Controller
|
||||
public class BiliUserController {
|
||||
@@ -24,11 +45,103 @@ public class BiliUserController {
|
||||
JSONArray array = new JSONArray();
|
||||
List<LoginUserDatabaseBean> allUser = loginService.getAllUser();
|
||||
for (LoginUserDatabaseBean bean : allUser) {
|
||||
JSONObject json=new JSONObject();
|
||||
json.put("uid",bean.getUserInfo().getMid());
|
||||
json.put("uname",bean.getUserInfo().getUname());
|
||||
array.add(json);
|
||||
LoginApi api = BiliLoginNetApiManager.getInstance().getLoginApi(bean.getUserInfo().getMid());
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("uid", bean.getUserInfo().getMid());
|
||||
json.put("uname", bean.getUserInfo().getUname());
|
||||
json.put("face",bean.getUserInfo().getFace());
|
||||
json.put("sql_time",bean.getSql_time());
|
||||
try {
|
||||
CheckCookieBean cookie = api.checkCookie().execute().body().getData();
|
||||
if (cookie == null) {
|
||||
json.put("status", "无效");
|
||||
} else {
|
||||
json.put("status", cookie.isRefresh()?"待刷新":"正常");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(e);
|
||||
json.put("status", "无效");
|
||||
}
|
||||
array.add(json);
|
||||
}
|
||||
return ResultData.success(array);
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@RequestMapping("/user/login")
|
||||
public JSONObject login(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession();
|
||||
Object loginToken = session.getAttribute("loginToken");
|
||||
if (loginToken == null) {
|
||||
loginToken = UUID.randomUUID().toString();
|
||||
session.setAttribute("loginToken", loginToken);
|
||||
}
|
||||
return ResultData.success(login(loginToken.toString()));
|
||||
|
||||
}
|
||||
|
||||
private JSONObject login(String loginToken) {
|
||||
String ret = RedisTools.get(loginToken);
|
||||
if (StringUtils.hasText(ret)) {
|
||||
JSONObject json = JSONObject.parseObject(ret);
|
||||
if (json.getIntValue("code") == BiliLoginNetApiManager.LOGIN_SUCCESS) {
|
||||
RedisTools.remove(loginToken);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
BiliLoginNetApiManager.getInstance().login(new HttpCallback<LoginCookieDatabaseBean>() {
|
||||
@Override
|
||||
public void onResponse(Headers headers, int code, String status, LoginCookieDatabaseBean response, String rawResponse) {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("code", code);
|
||||
if (code == BiliLoginNetApiManager.LOGIN_QRCODE) {
|
||||
json.put("qrcode", rawResponse);
|
||||
} else if (code == BiliLoginNetApiManager.LOGIN_SUCCESS) {
|
||||
Response<HttpBody<UserInfoBean>> execute = null;
|
||||
try {
|
||||
execute = BiliUserNetApiManager.getInstance().getUserApi(response).getUserInfo().execute();
|
||||
if (execute.isSuccessful()) {
|
||||
if (execute.body() != null) {
|
||||
UserInfoBean data = execute.body().getData();
|
||||
LoginUserDatabaseBean userBean = new LoginUserDatabaseBean(data);
|
||||
BiliBiliLoginDatabase.getInstance().initData(response, userBean).close();
|
||||
json.put("user", data);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
RedisTools.set(loginToken, json.toString(), 60 * 60);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
|
||||
}
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
LiveLoginService loginService = new LiveLoginService();
|
||||
/* List<LoginUserDatabaseBean> allUser = loginService.getAllUser();
|
||||
for (LoginUserDatabaseBean user : allUser) {
|
||||
System.out.println(user.getUserInfo().getMid());
|
||||
LoginApi api = BiliLoginNetApiManager.getInstance().getLoginApi(user.getUserInfo().getMid());
|
||||
try {
|
||||
CheckCookieBean cookie = api.checkCookie().execute().body().getData();
|
||||
System.out.println("cookie = " + cookie);
|
||||
if(cookie.isRefresh()){
|
||||
BiliCookieManager.resetCookie(loginService.getCookie(user.getUserInfo().getMid()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(e);
|
||||
}finally {
|
||||
return;
|
||||
}
|
||||
}*/
|
||||
HttpLoggingInterceptor.setLog(true);
|
||||
BiliCookieManager.resetCookie(loginService.getCookie("96300"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.yutou.bilibili.services;
|
||||
|
||||
import com.yutou.biliapi.api.LoginApi;
|
||||
import com.yutou.biliapi.bean.login.LoginCookieDatabaseBean;
|
||||
import com.yutou.biliapi.bean.login.LoginInfoBean;
|
||||
import com.yutou.biliapi.bean.login.LoginUserDatabaseBean;
|
||||
import com.yutou.biliapi.bean.login.QRCodeGenerateBean;
|
||||
@@ -56,4 +57,10 @@ public class LiveLoginService {
|
||||
|
||||
return loginDatabase.getAllUser();
|
||||
}
|
||||
public List<LoginCookieDatabaseBean> getAllUserCookie(){
|
||||
return loginDatabase.getAllCookies();
|
||||
}
|
||||
public LoginCookieDatabaseBean getCookie(String userId) {
|
||||
return loginDatabase.getCookie(userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ public class Log {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Object obj : log) {
|
||||
if (!sb.isEmpty()) {
|
||||
sb.append("\n");
|
||||
sb.append("|");
|
||||
}
|
||||
sb.append(obj);
|
||||
}
|
||||
|
||||
116
src/main/java/com/yutou/common/utils/WebClient.java
Normal file
116
src/main/java/com/yutou/common/utils/WebClient.java
Normal file
@@ -0,0 +1,116 @@
|
||||
package com.yutou.common.utils;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.openqa.selenium.Cookie;
|
||||
import org.openqa.selenium.Proxy;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
import org.openqa.selenium.remote.CapabilityType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class WebClient {
|
||||
private WebDriver driver;
|
||||
private static WebClient instance;
|
||||
|
||||
public static WebClient getInstance() {
|
||||
if (instance == null || instance.driver == null) {
|
||||
instance = new WebClient();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public WebDriver getWebDriver() {
|
||||
if (driver == null) {
|
||||
driver = new ChromeDriver(getOptions());
|
||||
}
|
||||
return driver;
|
||||
}
|
||||
|
||||
public void quit() {
|
||||
driver.quit();
|
||||
driver = null;
|
||||
}
|
||||
|
||||
private WebClient() {
|
||||
//System.setProperty("webdriver.http.factory", "jdk-http-client");
|
||||
System.setProperty("webdriver.chrome.driver",
|
||||
ConfigTools.load(ConfigTools.CONFIG, "chromedriver", String.class));
|
||||
// System.setProperty("webdriver.chrome.whitelistedIps", "");
|
||||
// java.util.logging.Logger.getLogger("org.openqa.selenium").setLevel(Level.OFF);
|
||||
}
|
||||
|
||||
|
||||
public static List<Cookie> loadCookie(JSONArray array) {
|
||||
List<Cookie> list = new ArrayList<>();
|
||||
|
||||
for (Object o : array) {
|
||||
JSONObject json = (JSONObject) o;
|
||||
boolean containsDate = json.containsKey("expirationDate");
|
||||
long t = 0;
|
||||
if (containsDate) {
|
||||
String _time = json.getString("expirationDate");
|
||||
if (_time.contains(".")) {
|
||||
_time = _time.split("\\.")[0];
|
||||
}
|
||||
t = Long.parseLong(_time);
|
||||
} else {
|
||||
t = (System.currentTimeMillis()) / 1000;
|
||||
}
|
||||
t *= 1000;
|
||||
|
||||
Cookie cookie = new Cookie(json.getString("name"),
|
||||
json.getString("value"),
|
||||
json.getString("domain"),
|
||||
json.getString("path"),
|
||||
new Date(t),
|
||||
json.getBooleanValue("secure"),
|
||||
json.getBooleanValue("httpOnly")
|
||||
|
||||
);
|
||||
list.add(cookie);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
static boolean headless = false;
|
||||
|
||||
public void setHeadless(boolean headless) {
|
||||
WebClient.headless = headless;
|
||||
}
|
||||
|
||||
public ChromeOptions getOptions() {
|
||||
ChromeOptions options = new ChromeOptions();
|
||||
options.addArguments("--remote-allow-origins=*");
|
||||
// options.addArguments("--disable-gpu");
|
||||
// options.addArguments("blink-settings=imagesEnabled=false");
|
||||
String headless = RedisTools.get("chromedrive_headless");
|
||||
String proxy = RedisTools.get("chromedrive_proxy");
|
||||
if ("true".equals(proxy)) {
|
||||
String url = "http://127.0.0.1:7890";
|
||||
Proxy _proxy = new Proxy();
|
||||
_proxy.setHttpProxy(url);
|
||||
_proxy.setSslProxy(url);
|
||||
options.setCapability(CapabilityType.PROXY, _proxy);
|
||||
}
|
||||
if ("true".equals(headless) || WebClient.headless) {
|
||||
options.addArguments("--headless");
|
||||
}
|
||||
options.addArguments("--no-sandbox");
|
||||
// options.addArguments("--incognito");
|
||||
options.addArguments("--disable-plugins");
|
||||
options.addArguments("--lang=zh-CN");
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
WebDriver driver1 = getInstance().getWebDriver();
|
||||
|
||||
driver1.get("https://www.tsdm39.net/forum.php");
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
server.port=8880
|
||||
server.port=8080
|
||||
logging.file.path=./logs
|
||||
logging.level.com.log.controller = trace
|
||||
|
||||
Reference in New Issue
Block a user