Skip to content
This repository was archived by the owner on Jun 24, 2024. It is now read-only.

Commit b7c0273

Browse files
committed
feat: Made GetUserIdFromUserNameAsync retry Added the ability to post a image with message
1 parent abea310 commit b7c0273

File tree

8 files changed

+275
-21
lines changed

8 files changed

+275
-21
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ Don't forget to give the project a star! Thanks again!
107107
<!-- CONTACT -->
108108
## Contact
109109

110-
Your Name - [@tidusjar](https://twitter.com/@tidusjar) - tidusjar@gmail.com
110+
Jamie - [@tidusjar](https://twitter.com/@tidusjar) - tidusjar@gmail.com
111111

112112
Project Link: [https://github.com/tidusjar/Threads.Api](https://github.com/tidusjar/Threads.Api)
113113

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
namespace Threads.Api.Tests;
2+
3+
[TestFixture]
4+
[Ignore("yeah")]
5+
public class IntegrationTests
6+
{
7+
private ThreadsApi _subject;
8+
9+
private readonly string? _username = Environment.GetEnvironmentVariable("TEST_USERNAME");
10+
private readonly string? _password = Environment.GetEnvironmentVariable("TEST_PASSWORD");
11+
12+
[SetUp]
13+
public void Setup()
14+
{
15+
_subject = new ThreadsApi(new HttpClient());
16+
if (string.IsNullOrEmpty(_username))
17+
{
18+
throw new ArgumentNullException(nameof(_username));
19+
}
20+
21+
if (string.IsNullOrEmpty(_password))
22+
{
23+
throw new ArgumentNullException(nameof(_password));
24+
}
25+
}
26+
27+
[Test]
28+
[Ignore("Locks me out")]
29+
public async Task Login_Test()
30+
{
31+
var result = await _subject.LoginAsync(_username, _password);
32+
33+
Assert.Multiple(() =>
34+
{
35+
Assert.That(result, Is.Not.Null);
36+
Assert.That(result.Token, Is.Not.Null.Or.Empty);
37+
Assert.That(result.UserId, Is.Not.Null.Or.Empty);
38+
});
39+
}
40+
41+
[Test]
42+
[Ignore("Hit and miss")]
43+
public async Task GetUserIdFromUserName_Test()
44+
{
45+
var result = await _subject.GetUserIdFromUserNameAsync(_username);
46+
47+
Assert.Multiple(() =>
48+
{
49+
Assert.That(result, Is.Not.Null);
50+
Assert.That(result.Token, Is.Not.Null);
51+
Assert.That(result.UserId, Is.EqualTo(3897985));
52+
});
53+
}
54+
}

src/Threads.Api/IThreadsApi.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ public interface IThreadsApi
6666
/// <returns>ID of the post</returns>
6767
Task<string> PostAsync(string userId, string message, string authToken, CancellationToken cancellationToken = default);
6868

69+
Task<string> PostImageAsync(string username, string message, string authToken, ImageUploadRequest image, CancellationToken cancellationToken = default);
70+
6971
/// <summary>
7072
/// Follows a user
7173
/// </summary>
@@ -85,4 +87,5 @@ public interface IThreadsApi
8587
/// <param name="cancellationToken"><see cref="CancellationToken"/></param>
8688
/// <returns>true if success else false</returns>
8789
Task<bool> UnFollowAsync(int userId, string token, string authToken, CancellationToken cancellationToken = default);
90+
8891
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace Threads.Api.Models;
2+
3+
4+
public class ImageUploadRequest
5+
{
6+
/// <summary>
7+
/// Can be either a local path or remote
8+
/// </summary>
9+
public string Path { get; set; }
10+
/// <summary>
11+
/// Byte[] of the image content
12+
/// </summary>
13+
public byte[] Content { get; set; }
14+
/// <summary>
15+
/// Use in conjunction with <see cref="Content"/>
16+
/// </summary>
17+
public string MimeType { get; set; }
18+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Threads.Api.Models.Response;
2+
3+
internal class ImageUploadResponse
4+
{
5+
public string upload_id { get; set; }
6+
public string status { get; set; }
7+
}

src/Threads.Api/Threads.Api.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<Title>Threads.Net API</Title>
99
<Description>An unofficial Threads.Net API</Description>
1010
<PackageProjectUrl>https://github.com/tidusjar/Threads.Net</PackageProjectUrl>
11-
<PackageVersion>1.2.0-alpha</PackageVersion>
11+
<PackageVersion>1.2.1-alpha</PackageVersion>
1212
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
1313
<PackageTags>Threads; twitter; meta; insta; api; threads.net; instagram</PackageTags>
1414
<RepositoryUrl>https://github.com/tidusjar/Threads.Api/</RepositoryUrl>

src/Threads.Api/ThreadsApi.cs

Lines changed: 176 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.Security.Cryptography;
1212
using System.Text;
1313
using System.Text.Json;
14+
using System.Text.Json.Serialization;
1415
using System.Text.RegularExpressions;
1516
using System.Threading;
1617
using System.Threading.Tasks;
@@ -30,6 +31,7 @@ public partial class ThreadsApi : IThreadsApi
3031

3132
private readonly string LoginUrl = $"{BaseApiUrl}/bloks/apps/com.bloks.www.bloks.caa.login.async.send_login_request/";
3233
private readonly string PostUrl = $"{BaseApiUrl}/media/configure_text_only_post/";
34+
private readonly string PostImageUrl = $"{BaseApiUrl}/media/configure_text_post_app_feed/";
3335

3436
public ThreadsApi(HttpClient httpClient)
3537
{
@@ -44,6 +46,39 @@ public async Task<UserIdResponse> GetUserIdFromUserNameAsync(string username, Ca
4446
throw new ArgumentNullException(nameof(username));
4547
}
4648

49+
try
50+
{
51+
var userResult = await GetUserIdFromUserNameInternal(username, cancellationToken);
52+
53+
if (int.TryParse(userResult.UserId, out int value))
54+
{
55+
return new UserIdResponse
56+
{
57+
Token = userResult.LsdToken,
58+
UserId = value
59+
};
60+
}
61+
}
62+
catch (UserNotFoundException)
63+
{
64+
var userResult = await GetUserIdFromUserNameInternal(username, cancellationToken);
65+
66+
if (int.TryParse(userResult.UserId, out int value))
67+
{
68+
return new UserIdResponse
69+
{
70+
Token = userResult.LsdToken,
71+
UserId = value
72+
};
73+
}
74+
}
75+
76+
77+
throw new UserNotFoundException(username);
78+
}
79+
80+
private async Task<(string UserId, string LsdToken)> GetUserIdFromUserNameInternal(string username, CancellationToken cancellationToken)
81+
{
4782
var request = new HttpRequestMessage(HttpMethod.Get, $"{_url}@{username}");
4883
GetDefaultHeaders(null, request);
4984

@@ -74,16 +109,7 @@ public async Task<UserIdResponse> GetUserIdFromUserNameAsync(string username, Ca
74109
throw new UserNotFoundException(username);
75110
}
76111

77-
if (int.TryParse(userID, out int value))
78-
{
79-
return new UserIdResponse
80-
{
81-
Token = lsdToken,
82-
UserId = value
83-
};
84-
}
85-
86-
throw new UserNotFoundException(username);
112+
return (userID, lsdToken);
87113
}
88114

89115

@@ -390,6 +416,65 @@ public async Task<string> PostAsync(string username, string message, string auth
390416
return postData?.media?.id;
391417
}
392418

419+
public async Task<string> PostImageAsync(string username, string message, string authToken, ImageUploadRequest image, CancellationToken cancellationToken = default)
420+
{
421+
if (string.IsNullOrEmpty(username))
422+
{
423+
throw new ArgumentNullException(username);
424+
}
425+
if (string.IsNullOrWhiteSpace(message))
426+
{
427+
throw new ArgumentNullException(message);
428+
}
429+
if (string.IsNullOrWhiteSpace(authToken))
430+
{
431+
throw new ArgumentNullException(authToken);
432+
}
433+
434+
var userId = await GetUserIdFromUserNameAsync(username, cancellationToken);
435+
436+
var imageUploadResult = await UploadImageAsync(image, authToken, cancellationToken);
437+
438+
var data = new
439+
{
440+
text_post_app_info = new { reply_control = 0 },
441+
timezone_offset = "3600",
442+
source_type = '4',
443+
_uid = userId.UserId,
444+
device_id = _deviceId,
445+
caption = message,
446+
upload_id = imageUploadResult.upload_id,
447+
device = new
448+
{
449+
manufacturer = "OnePlus",
450+
model = "ONEPLUS+A3010",
451+
os_version = 25,
452+
os_release = "7.1.1",
453+
},
454+
publish_mode = "text_post",
455+
scene_capture_type = string.Empty,
456+
};
457+
458+
459+
var payload = $"signed_body=SIGNATURE.{JsonSerializer.Serialize(data)}";
460+
461+
var request = new HttpRequestMessage(HttpMethod.Post, new Uri(PostImageUrl))
462+
{
463+
Content = new StringContent(payload)
464+
};
465+
GetAppHeaders(request, authToken);
466+
request.Content.Headers.Clear();
467+
request.Content.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
468+
request.Content.Headers.Add("Response-Type", "text");
469+
470+
var response = await _client.SendAsync(request, cancellationToken).ConfigureAwait(false);
471+
472+
var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
473+
var postData = await JsonSerializer.DeserializeAsync<PostResponse>(stream).ConfigureAwait(false);
474+
475+
return postData?.media?.id;
476+
}
477+
393478
/// <inheritdoc/>
394479
public Task<bool> FollowAsync(int userId, string token, string authToken, CancellationToken cancellationToken = default)
395480
{
@@ -423,6 +508,87 @@ private async Task<bool> FollowUnfollowInternal(string url, int userId, string t
423508
return data?.IsSuccess ?? false;
424509
}
425510

511+
private async Task<ImageUploadResponse> UploadImageAsync(ImageUploadRequest image, string authToken, CancellationToken cancellationToken = default)
512+
{
513+
string uploadId = DateTime.Now.Ticks.ToString();
514+
string name = $"{uploadId}_0_{new Random().Next(100000000, 999999999)}";
515+
string url = $"https://www.instagram.com/rupload_igphoto/{name}";
516+
517+
byte[] imageBytes;
518+
string mime_type;
519+
520+
if (!string.IsNullOrEmpty(image.Path))
521+
{
522+
bool isFilePath = !image.Path.StartsWith("http");
523+
if (isFilePath)
524+
{
525+
imageBytes = await File.ReadAllBytesAsync(image.Path).ConfigureAwait(false);
526+
mime_type = "image/png" ?? "application/octet-stream";
527+
}
528+
else
529+
{
530+
HttpResponseMessage imageResponse = await _client.GetAsync(image.Path).ConfigureAwait(false);
531+
imageBytes = await imageResponse.Content.ReadAsByteArrayAsync();
532+
mime_type = imageResponse.Content.Headers.ContentType.MediaType;
533+
}
534+
}
535+
else
536+
{
537+
imageBytes = image.Content;
538+
mime_type = image.MimeType ?? "application/octet-stream";
539+
}
540+
541+
Dictionary<string, string> x_instagram_rupload_params = new Dictionary<string, string>
542+
{
543+
{ "upload_id", uploadId },
544+
{ "media_type", "1" },
545+
{ "sticker_burnin_params", "[]" },
546+
{ "image_compression", "{\"lib_name\":\"moz\",\"lib_version\":\"3.1.m\",\"quality\":\"80\"}" },
547+
{ "xsharing_user_ids", "[]" },
548+
{ "retry_context", "{\"num_step_auto_retry\":\"0\",\"num_reupload\":\"0\",\"num_step_manual_retry\":\"0\"}" },
549+
{ "IG-FB-Xpost-entry-point-v2", "feed" }
550+
};
551+
552+
int contentLength = imageBytes.Length;
553+
Dictionary<string, string> imageHeaders = new Dictionary<string, string>
554+
{
555+
{ "X_FB_PHOTO_WATERFALL_ID", Guid.NewGuid().ToString() },
556+
{ "X-Entity-Type", mime_type ?? "image/jpeg" },
557+
{ "Offset", "0" },
558+
{ "X-Instagram-Rupload-Params", JsonSerializer.Serialize(x_instagram_rupload_params) },
559+
{ "X-Entity-Name", name },
560+
{ "X-Entity-Length", contentLength.ToString() },
561+
{ "Accept-Encoding", "gzip" }
562+
};
563+
564+
Dictionary<string, string> contentHeaders = new Dictionary<string, string>
565+
{
566+
{ "Content-Type", "application/octet-stream" },
567+
{ "Content-Length", contentLength.ToString() },
568+
};
569+
570+
571+
572+
var request = new HttpRequestMessage(HttpMethod.Post, new Uri(url));
573+
574+
GetDefaultHeaders(null, request, authToken);
575+
request.Content = new ByteArrayContent(imageBytes);
576+
//request.Content.Headers.Clear();
577+
foreach (KeyValuePair<string, string> header in contentHeaders)
578+
{
579+
request.Content.Headers.Add(header.Key, header.Value);
580+
}
581+
foreach (KeyValuePair<string, string> header in imageHeaders)
582+
{
583+
request.Headers.Add(header.Key, header.Value);
584+
}
585+
586+
var response = await _client.SendAsync(request, cancellationToken).ConfigureAwait(false);
587+
var data = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
588+
589+
return await JsonSerializer.DeserializeAsync<ImageUploadResponse>(data, cancellationToken: cancellationToken).ConfigureAwait(false);
590+
}
591+
426592
private void GetDefaultHeaders(string token, HttpRequestMessage request, string authToken = default)
427593
{
428594
GetAppHeaders(request, authToken);

src/Threads.Example.App/Program.cs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,24 @@ static async Task Main(string[] args)
88
{
99
IThreadsApi api = new ThreadsApi(new HttpClient());
1010

11-
var username = "tidusjar";
11+
var username = Environment.GetEnvironmentVariable("TEST_USERNAME");
1212

13-
var r = await api.GetUserIdFromUserNameAsync(username);
14-
var userProfile = await api.GetUserProfileAsync(username, r.UserId, r.Token);
15-
var threads = await api.GetThreadsAsync(username, r.UserId, r.Token);
16-
var replies = await api.GetUserRepliesAsync(username, r.UserId, r.Token);
17-
var authToken = await api.LoginAsync("tidusjar", "pass");
18-
await api.PostAsync(username, "Hello!", authToken.Token);
13+
//var r = await api.GetUserIdFromUserNameAsync(username);
14+
//var userProfile = await api.GetUserProfileAsync(username, r.UserId, r.Token);
15+
//var threads = await api.GetThreadsAsync(username, r.UserId, r.Token);
16+
//var replies = await api.GetUserRepliesAsync(username, r.UserId, r.Token);
17+
var authToken = await api.LoginAsync("tidusjar", Environment.GetEnvironmentVariable("TEST_PASSWORD"));
18+
//await api.PostAsync(username, "Hello!", authToken.Token);
1919

20-
var userNameToFollow = "zuck";
21-
var userToFollow = await api.GetUserIdFromUserNameAsync(userNameToFollow);
20+
//var userNameToFollow = "zuck";
21+
var userToFollow = await api.GetUserIdFromUserNameAsync("loganpaul");
2222
await api.FollowAsync(userToFollow.UserId, userToFollow.Token, authToken.Token);
2323

24+
25+
var a = await api.PostImageAsync(username, "This message was posted from my C# Wrapper around the Threads API! https://github.com/tidusjar/Threads.Api", authToken.Token, new Api.Models.ImageUploadRequest
26+
{
27+
Path = "https://raw.githubusercontent.com/tidusjar/Threads.Api/main/assets/example.png"
28+
});
29+
2430
}
2531
}

0 commit comments

Comments
 (0)