Files
douyin/service/DouyinUserSearchClient.cs
T

218 lines
9.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using dy.net.model.dto;
using dy.net.utils;
using Newtonsoft.Json;
namespace dy.net.service
{
public sealed class DouyinUserSearchClient
{
private const string SearchPath = "/aweme/v1/web/discover/search/";
private readonly IHttpClientFactory _clientFactory;
private readonly DouyinABogusSigner _signer = new();
public DouyinUserSearchClient(IHttpClientFactory clientFactory) => _clientFactory = clientFactory;
public async Task<List<DouyinFollowCandidate>> SearchAsync(
string douyinNo,
string cookie,
CancellationToken cancellationToken = default)
{
var parameters = BuildParameters(douyinNo, cookie);
var unsignedQuery = BuildQueryString(parameters);
var signature = _signer.Sign(unsignedQuery);
var requestUri = SearchPath + "?" + unsignedQuery + "&a_bogus=" + Encode(signature);
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.Referrer = new Uri("https://www.douyin.com/root/search/" + Encode(douyinNo) + "?type=user");
request.Headers.TryAddWithoutValidation("Accept", "*/*");
request.Headers.TryAddWithoutValidation("Cookie", cookie);
var client = _clientFactory.CreateClient(DouyinRequestParamManager.DY_HTTP_CLIENT);
try
{
using var response = await client.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
if (!response.IsSuccessStatusCode)
throw new InvalidOperationException($"抖音用户搜索请求失败(HTTP {(int)response.StatusCode}");
var content = await response.Content.ReadAsStringAsync(cancellationToken);
SearchResponse payload;
try
{
payload = JsonConvert.DeserializeObject<SearchResponse>(content);
}
catch (JsonException)
{
throw new InvalidOperationException("抖音返回了风控验证页面,请稍后重试或使用手动添加");
}
if (payload == null)
throw new InvalidOperationException("抖音用户搜索没有返回有效数据");
if (payload.StatusCode != 0)
throw new InvalidOperationException("抖音用户搜索被拒绝,请确认 Cookie 有效后重试");
return (payload.Users ?? new List<SearchUserEntry>())
.Select(x => x.User)
.Where(x => x != null &&
!string.IsNullOrWhiteSpace(x.SecUid) &&
!string.IsNullOrWhiteSpace(x.Uid) &&
!string.IsNullOrWhiteSpace(x.Nickname))
.Select(x => new DouyinFollowCandidate
{
SecUid = x.SecUid,
UperId = x.Uid,
UniqueId = x.UniqueId,
ShortId = x.ShortId,
DouyinNo = !string.IsNullOrWhiteSpace(x.UniqueId) ? x.UniqueId : x.ShortId,
UperName = x.Nickname,
UperAvatar = x.Avatar?.Urls?.FirstOrDefault() ?? string.Empty,
Signature = x.Signature,
Enterprise = !string.IsNullOrWhiteSpace(x.Enterprise) ? x.Enterprise : x.CustomVerify,
FollowerCount = x.FollowerCount
})
.GroupBy(x => x.SecUid, StringComparer.Ordinal)
.Select(x => x.First())
.Take(10)
.ToList();
}
catch (HttpRequestException)
{
// 不向上抛出包含查询 URI 的原始异常,避免 msToken 出现在全局日志中。
throw new InvalidOperationException("抖音用户搜索网络请求失败,请稍后重试");
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
throw new InvalidOperationException("抖音用户搜索超时,请稍后重试");
}
}
private static List<KeyValuePair<string, string>> BuildParameters(string douyinNo, string cookie)
{
var cookies = ParseCookie(cookie);
return new List<KeyValuePair<string, string>>
{
new("device_platform", "webapp"),
new("aid", "6383"),
new("channel", "channel_pc_web"),
new("update_version_code", "170400"),
new("pc_client_type", "1"),
new("pc_libra_divert", "Windows"),
new("support_h265", "1"),
new("support_dash", "1"),
new("version_code", "170400"),
new("version_name", "17.4.0"),
new("cookie_enabled", "true"),
new("screen_width", "1536"),
new("screen_height", "864"),
new("browser_language", "zh-CN"),
new("browser_platform", "Win32"),
new("browser_name", "Chrome"),
new("browser_version", "119.0.0.0"),
new("browser_online", "true"),
new("engine_name", "Blink"),
new("engine_version", "119.0.0.0"),
new("os_name", "Windows"),
new("os_version", "10"),
new("cpu_core_num", "16"),
new("device_memory", "8"),
new("platform", "PC"),
new("downlink", "10"),
new("effective_type", "4g"),
new("round_trip_time", "200"),
new("uifid", CookieValue(cookies, "UIFID", "UIFID_TEMP")),
new("msToken", CookieValue(cookies, "msToken")),
new("pc_search_top_1_params", "{\"enable_ai_search_top_1\":1}"),
new("search_channel", "aweme_user_web"),
new("keyword", douyinNo),
new("search_source", "switch_tab"),
new("query_correct_type", "1"),
new("is_filter_search", "0"),
new("from_group_id", string.Empty),
new("disable_rs", "0"),
new("offset", "0"),
new("count", "10"),
new("need_filter_settings", "0"),
new("list_type", "single")
};
}
private static Dictionary<string, string> ParseCookie(string cookie)
{
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var part in (cookie ?? string.Empty).Split(';', StringSplitOptions.RemoveEmptyEntries))
{
var separator = part.IndexOf('=');
if (separator <= 0) continue;
values[part[..separator].Trim()] = part[(separator + 1)..].Trim();
}
return values;
}
private static string CookieValue(Dictionary<string, string> cookies, params string[] names)
{
foreach (var name in names)
if (cookies.TryGetValue(name, out var value)) return value;
return string.Empty;
}
private static string BuildQueryString(IEnumerable<KeyValuePair<string, string>> parameters) =>
string.Join("&", parameters.Select(x => Encode(x.Key) + "=" + Encode(x.Value ?? string.Empty)));
private static string Encode(string value) => Uri.EscapeDataString(value ?? string.Empty);
private sealed class SearchResponse
{
[JsonProperty("status_code")]
public int StatusCode { get; set; }
[JsonProperty("user_list")]
public List<SearchUserEntry> Users { get; set; }
}
private sealed class SearchUserEntry
{
[JsonProperty("user_info")]
public SearchUser User { get; set; }
}
private sealed class SearchUser
{
[JsonProperty("sec_uid")]
public string SecUid { get; set; }
[JsonProperty("uid")]
public string Uid { get; set; }
[JsonProperty("unique_id")]
public string UniqueId { get; set; }
[JsonProperty("short_id")]
public string ShortId { get; set; }
[JsonProperty("nickname")]
public string Nickname { get; set; }
[JsonProperty("signature")]
public string Signature { get; set; }
[JsonProperty("enterprise_verify_reason")]
public string Enterprise { get; set; }
[JsonProperty("custom_verify")]
public string CustomVerify { get; set; }
[JsonProperty("follower_count")]
public long FollowerCount { get; set; }
[JsonProperty("avatar_thumb")]
public SearchAvatar Avatar { get; set; }
}
private sealed class SearchAvatar
{
[JsonProperty("url_list")]
public List<string> Urls { get; set; }
}
}
}