2024-12-16 04:50:54 +01:00
using System.Net ;
using System.Net.Http.Headers ;
using System.Text ;
2024-11-22 09:55:08 +01:00
namespace Uwaa.Pleroma ;
2024-12-16 04:50:54 +01:00
delegate void AddPair ( string key , string value ) ;
2024-11-22 09:55:08 +01:00
/// <summary>
/// A pleroma client.
/// </summary>
public class Pleroma
{
static readonly JsonSerializerOptions SerializerOptions = new ( )
{
PropertyNameCaseInsensitive = true ,
2024-12-16 04:50:54 +01:00
NumberHandling = JsonNumberHandling . AllowReadingFromString ,
2024-11-22 09:55:08 +01:00
} ;
2024-12-16 04:50:54 +01:00
static string CreateQuery ( Action < AddPair > generator )
{
StringBuilder sb = new StringBuilder ( ) ;
void addPair ( string key , string value )
{
if ( sb . Length = = 0 )
sb . Append ( '?' ) ;
else
sb . Append ( '&' ) ;
sb . Append ( WebUtility . UrlEncode ( key ) ) ;
sb . Append ( '=' ) ;
sb . Append ( WebUtility . UrlEncode ( value ) ) ;
}
generator ( addPair ) ;
return sb . ToString ( ) ;
}
2024-11-22 09:55:08 +01:00
2024-12-16 04:50:54 +01:00
public HttpClient HttpClient ;
2024-11-22 09:55:08 +01:00
2024-12-16 04:54:10 +01:00
public Pleroma ( string host , string? authorization , string? userAgent = "Uwaa.Pleroma/0.0" )
2024-11-30 14:14:00 +01:00
{
2024-12-16 04:50:54 +01:00
UriBuilder builder = new UriBuilder ( ) ;
builder . Scheme = "https" ;
builder . Host = host ;
HttpClient = new HttpClient ( ) ;
2024-12-16 04:54:10 +01:00
HttpClient . DefaultRequestHeaders . Add ( "User-agent" , userAgent ) ;
2024-12-16 04:50:54 +01:00
HttpClient . BaseAddress = builder . Uri ;
HttpClient . DefaultRequestHeaders . Accept . Add ( new MediaTypeWithQualityHeaderValue ( "application/json" ) ) ;
if ( authorization ! = null )
HttpClient . DefaultRequestHeaders . Authorization = AuthenticationHeaderValue . Parse ( authorization ) ;
2024-11-30 14:14:00 +01:00
}
2024-12-16 04:50:54 +01:00
async Task < T ? > Retry < T > ( HttpRequestMessage req )
2024-11-22 09:55:08 +01:00
{
2024-12-11 03:49:46 +01:00
while ( true )
{
2024-12-16 04:50:54 +01:00
HttpResponseMessage res = await HttpClient . SendAsync ( req ) ;
2024-11-22 09:55:08 +01:00
2024-12-16 04:50:54 +01:00
if ( res . StatusCode = = HttpStatusCode . NotFound )
return default ;
2024-11-22 09:55:08 +01:00
2024-12-16 04:50:54 +01:00
if ( res . Content = = null )
throw new HttpRequestException ( "Server responded with no content" ) ;
2024-11-22 09:55:08 +01:00
2024-12-16 04:50:54 +01:00
string text = await res . Content . ReadAsStringAsync ( ) ;
2024-11-22 09:55:08 +01:00
2024-12-16 04:50:54 +01:00
if ( res . StatusCode is > = ( HttpStatusCode ) 400 and < ( HttpStatusCode ) 600 )
2024-11-22 09:55:08 +01:00
{
2024-12-11 03:49:46 +01:00
try
{
PleromaException ? err = JsonSerializer . Deserialize < PleromaException > ( text , SerializerOptions ) ;
if ( err ! = null & & err . Text ! = null )
{
if ( err . Text = = "Throttled" )
{
await Task . Delay ( 5000 ) ;
continue ; //Retry
}
else
{
throw err ;
}
}
}
catch ( JsonException )
{
//Not an error
}
2024-11-22 09:55:08 +01:00
}
2024-12-16 04:50:54 +01:00
if ( res . StatusCode is > = ( HttpStatusCode ) 200 and < ( HttpStatusCode ) 300 )
return JsonSerializer . Deserialize < T > ( text , SerializerOptions ) ? ? throw new HttpRequestException ( "Couldn't deserialize response" ) ;
2024-12-11 03:49:46 +01:00
else
2024-12-16 04:50:54 +01:00
throw new HttpRequestException ( "Unknown error occurred" ) ;
2024-12-11 03:49:46 +01:00
}
2024-11-22 09:55:08 +01:00
}
/// <summary>
/// Posts a status.
/// </summary>
2024-12-16 04:57:06 +01:00
/// <param name="content">Text content of the status.</param>
/// <param name="sensitive">Mark status and attached media as sensitive?</param>
/// <param name="expiresIn">The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour.</param>
/// <param name="replyTo">ID of the status being replied to, if status is a reply</param>
/// <param name="quoting"> ID of the status being quoted.</param>
/// <param name="language">ISO 639 language code for this status.</param>
/// <param name="visibility">Visibility of the posted status.</param>
2024-12-16 04:50:54 +01:00
/// <exception cref="HttpRequestException">Thrown if something goes wrong while uploading the status.</exception>
2024-12-11 03:49:46 +01:00
/// <returns>The status if posting was successful.</returns>
2024-12-16 04:50:54 +01:00
public Task < Status > PostStatus ( string content ,
bool sensitive = false ,
int? expiresIn = null ,
string? replyTo = null ,
string? quoting = null ,
string? language = null ,
StatusVisibility visibility = StatusVisibility . Public )
2024-11-22 09:55:08 +01:00
{
2024-12-16 04:50:54 +01:00
MemoryStream mem = new MemoryStream ( ) ;
{
Utf8JsonWriter writer = new Utf8JsonWriter ( mem , new JsonWriterOptions ( ) { SkipValidation = true } ) ;
writer . WriteStartObject ( ) ;
writer . WritePropertyName ( "status" ) ;
writer . WriteStringValue ( content ) ;
writer . WritePropertyName ( "content-type" ) ;
writer . WriteStringValue ( "text/plain" ) ;
if ( sensitive )
{
writer . WritePropertyName ( "sensitive" ) ;
writer . WriteBooleanValue ( true ) ;
}
if ( expiresIn . HasValue )
{
writer . WritePropertyName ( "expires_in" ) ;
writer . WriteNumberValue ( expiresIn . Value ) ;
}
if ( replyTo ! = null )
{
writer . WritePropertyName ( "in_reply_to_id" ) ;
writer . WriteStringValue ( replyTo ) ;
}
if ( quoting ! = null )
{
writer . WritePropertyName ( "quote_id" ) ;
writer . WriteStringValue ( quoting ) ;
}
if ( language ! = null )
{
writer . WritePropertyName ( "language" ) ;
writer . WriteStringValue ( language ) ;
}
if ( visibility ! = StatusVisibility . Public )
{
writer . WritePropertyName ( "visibility" ) ;
writer . WriteStringValue ( visibility . ToString ( ) . ToLowerInvariant ( ) ) ;
}
writer . WriteEndObject ( ) ;
writer . Flush ( ) ;
}
mem . Position = 0 ;
HttpRequestMessage req = new HttpRequestMessage ( HttpMethod . Post , "/api/v1/statuses" ) ;
req . Content = new StreamContent ( mem ) ;
req . Content . Headers . ContentType = new MediaTypeHeaderValue ( "application/json" ) ;
return Retry < Status > ( req ) ! ;
2024-11-22 09:55:08 +01:00
}
/// <summary>
/// Fetches the latest statuses from the public timeline.
/// </summary>
public Task < Status [ ] > GetTimeline ( )
{
//TODO: Parameters and selecting different timelines (home, public, bubble)
2024-12-16 04:50:54 +01:00
return Retry < Status [ ] > ( new HttpRequestMessage ( HttpMethod . Get , "/api/v1/timelines/public" ) ) ! ;
2024-11-22 09:55:08 +01:00
}
2024-12-16 04:50:54 +01:00
2024-11-22 09:55:08 +01:00
/// <summary>
/// Fetches the latest statuses from a user's timeline.
/// </summary>
public Task < Status [ ] > GetTimeline ( Account account )
= > GetTimeline ( account . ID ) ;
/// <summary>
/// Fetches the latest statuses from a user's timeline.
/// </summary>
public Task < Status [ ] > GetTimeline ( string account_id )
{
2024-12-16 04:50:54 +01:00
return Retry < Status [ ] > ( new HttpRequestMessage ( HttpMethod . Get , $"/api/v1/accounts/{account_id}/statuses" ) ) ! ;
2024-11-22 09:55:08 +01:00
}
/// <summary>
/// Fetches the account of the pleroma client.
/// </summary>
public Task < Account > GetAccount ( )
{
2024-12-16 04:50:54 +01:00
return Retry < Account > ( new HttpRequestMessage ( HttpMethod . Get , "/api/v1/accounts/verify_credentials" ) ) ! ;
2024-11-22 09:55:08 +01:00
}
/// <summary>
/// Fetches an account.
/// </summary>
public Task < Account ? > GetAccount ( string id )
{
2024-12-16 04:50:54 +01:00
return Retry < Account ? > ( new HttpRequestMessage ( HttpMethod . Get , $"/api/v1/accounts/{id}" ) ) ;
2024-11-22 09:55:08 +01:00
}
/// <summary>
/// Fetches the context of a status.
/// </summary>
public Task < Context ? > GetContext ( Status status )
= > GetContext ( status . ID ) ;
/// <summary>
/// Fetches the context of a status.
/// </summary>
public Task < Context ? > GetContext ( string status_id )
{
2024-12-16 04:50:54 +01:00
return Retry < Context ? > ( new HttpRequestMessage ( HttpMethod . Get , $"/api/v1/statuses/{status_id}/context" ) ) ;
2024-12-11 03:49:46 +01:00
}
/// <summary>
/// Fetches a status.
/// </summary>
public Task < Status ? > GetStatus ( string status_id )
{
2024-12-16 04:50:54 +01:00
return Retry < Status ? > ( new HttpRequestMessage ( HttpMethod . Get , $"/api/v1/statuses/{status_id}" ) ) ;
2024-11-30 14:14:00 +01:00
}
2024-11-30 14:14:20 +01:00
/// <summary>
/// Deletes a status.
/// </summary>
public Task Delete ( Status status )
= > Delete ( status . ID ) ;
/// <summary>
/// Deletes a status by ID.
/// </summary>
public Task Delete ( string status_id )
{
2024-12-16 04:54:10 +01:00
//TODO: Test
2024-12-16 04:50:54 +01:00
return Retry < object? > ( new HttpRequestMessage ( HttpMethod . Delete , $"/api/v1/statuses/{status_id}" ) ) ;
2024-12-11 03:49:46 +01:00
}
/// <summary>
/// Searches for an accounts, hashtags, and/or statuses.
/// </summary>
/// <param name="query">What to search for</param>
/// <param name="account_id">If provided, statuses returned will be authored only by this account</param>
/// <param name="type">Search type</param>
/// <param name="resolve">Attempt WebFinger lookup</param>
/// <param name="following">Only include accounts that the user is following</param>
/// <param name="max_id">Return items older than this ID</param>
/// <param name="min_id">Return the oldest items newer than this ID</param>
/// <param name="since_id">Return the newest items newer than this ID</param>
/// <param name="offset">Return items past this number of items</param>
/// <param name="limit">Maximum number of items to return. Will be ignored if it's more than 40</param>
/// <returns></returns>
public Task < SearchResults > Search ( string query ,
string? account_id = null ,
SearchType type = SearchType . All ,
bool resolve = false ,
bool following = false ,
string? max_id = null ,
string? min_id = null ,
string? since_id = null ,
int offset = 0 ,
int limit = 20 )
{
2024-12-16 04:50:54 +01:00
HttpRequestMessage req = new HttpRequestMessage ( HttpMethod . Get , "/api/v2/search" + CreateQuery ( addPair = >
{
addPair ( "q" , query ) ;
if ( account_id ! = null ) addPair ( "account_id" , account_id ) ;
if ( type ! = SearchType . All ) addPair ( "type" , type . ToString ( ) . ToLowerInvariant ( ) ) ;
if ( resolve ) addPair ( "resolve" , "true" ) ;
if ( following ) addPair ( "following" , "true" ) ;
if ( max_id ! = null ) addPair ( "max_id" , max_id ) ;
if ( min_id ! = null ) addPair ( "min_id" , min_id ) ;
if ( since_id ! = null ) addPair ( "since_id" , since_id ) ;
if ( offset > 0 ) addPair ( "offset" , offset . ToString ( ) ) ;
if ( limit ! = 20 ) addPair ( "limit" , limit . ToString ( ) ) ;
} ) ) ;
return Retry < SearchResults > ( req ) ! ;
2024-11-22 09:55:08 +01:00
}
}