2023-04-02 23:32:03 -04:00
using System.Collections.Generic ;
2023-04-03 00:36:53 -04:00
using System.ComponentModel ;
2023-04-02 23:32:03 -04:00
using Newtonsoft.Json ;
using System.Net.Http.Json ;
2023-04-03 13:09:39 -04:00
using ttrss_co_client.ttrss.datastructures ;
2023-04-02 23:32:03 -04:00
// https://tt-rss.org/wiki/ApiReference
2023-04-03 16:37:59 -04:00
//TODO: a lot of these are a given at API level 1, so double check all necessary API levels
2023-04-02 23:32:03 -04:00
namespace ttrss_co_client.ttrss
{
public class ApiClient
{
public Uri BaseURI { get ; private set ; }
private HttpClient httpClient { get ; set ; }
private string SessionId { get ; set ; } = null ;
2023-04-03 00:36:53 -04:00
private int api_level { get ; set ; }
2023-04-02 23:32:03 -04:00
public ApiClient ( Uri baseUri )
{
BaseURI = baseUri ;
httpClient = new HttpClient ( ) { BaseAddress = baseUri } ;
}
public async Task Login ( string username , string password )
{
var json = new
{
op = "login" ,
user = username ,
password = password
} ;
var content = JsonContent . Create ( json ) ;
var response = await ( await httpClient . PostAsync ( BaseURI , content ) ) . Content . ReadAsStringAsync ( ) ;
var loginResult = JsonConvert . DeserializeObject < ttrss . messages . LoginResponse > ( response ) ;
if ( loginResult . status = = 0 )
{
SessionId = loginResult . content . session_id ;
2023-04-03 16:37:59 -04:00
if ( loginResult . content . api_level = = null )
{
throw new NotImplementedException ( $"api doesn't report an api level - unsupported. api level 1 is version 1.5.8, so {BaseURI} might be extremely old. (or maybe this library is extremely old and ttrss changed again?)" ) ;
}
else
{
api_level = loginResult . content . api_level . Value ;
}
2023-04-02 23:32:03 -04:00
Console . WriteLine ( SessionId ) ;
}
else
{
throw new NotImplementedException ( ) ;
}
}
public async Task < int > GetApiLevel ( )
{
2023-04-03 16:37:59 -04:00
//1.5.8 = api level 1, any lower than that, unsupported.
//???? = 2
//???? = 3
//1.6.0 = 4
//1.7.6 = 5
//1.8 = ????
//???? = 6
//???? = 7
//???? = 8
//1.14 = 9
2023-04-02 23:32:03 -04:00
assertInitialized ( ) ;
2023-04-03 00:36:53 -04:00
return await oneValueGet < int > ( "getApiLevel" , "level" ) ;
2023-04-02 23:32:03 -04:00
}
public async Task < bool > Logout ( )
{
assertInitialized ( ) ;
2023-04-03 00:36:53 -04:00
return ( await oneValueGet < string > ( "logout" , "status" ) ) ? . ToLower ( ) = = "ok" ;
2023-04-02 23:32:03 -04:00
}
public async Task < bool > IsLoggedIn ( )
{
2023-04-03 00:36:53 -04:00
//assertInitialized();
return ( await oneValueGet < bool > ( "isLoggedIn" , "status" ) ) ;
2023-04-02 23:32:03 -04:00
}
public async Task < int > GetUnread ( )
{
assertInitialized ( ) ;
2023-04-03 00:36:53 -04:00
return await oneValueGet < int > ( "getUnread" , "unread" ) ;
2023-04-02 23:32:03 -04:00
}
2023-04-03 00:36:53 -04:00
2023-04-03 13:09:39 -04:00
///<summary>at least in my installation, it doesn't seem to respect the parameters I give it, be it curl or here.</summary>
public async Task < IEnumerable < CounterInfo > > GetCounters ( bool feeds = true , bool labels = true , bool categories = true , bool tags = false )
{
assertInitialized ( ) ;
var output_mode = "" ;
if ( feeds ) output_mode + = "f" ;
if ( labels ) output_mode + = "l" ;
if ( categories ) output_mode + = "c" ;
if ( tags ) output_mode + = "t" ;
var json = JsonContent . Create ( new
{
op = "getCounters" ,
sid = this . SessionId ,
output_mode = output_mode
} ) ;
var response = await ( await httpClient . PostAsync ( BaseURI , json ) ) . Content . ReadAsStringAsync ( ) ;
var apiResult = JsonConvert . DeserializeObject < ttrss . messages . CounterInfoResponse > ( response ) ;
return apiResult . content ;
}
2023-04-03 13:50:33 -04:00
///<param name="limit">0 for all</param>
///<param name="offset">skip this amount first</param>
///<param name="include_nested">idk, doesn't affect what the documentation says it should</param>
public async Task < IEnumerable < Feed > > GetFeeds ( int cat_id = 0 , bool unread_only = false , uint limit = 0 , int offset = 0 /*, bool include_nested*/ )
2023-04-02 23:32:03 -04:00
{
assertInitialized ( ) ;
2023-04-03 13:50:33 -04:00
if ( cat_id < - 2 )
{
if ( cat_id = = - 3 | | cat_id = = - 4 )
{
assertApiLevel ( 4 ) ;
}
else
{
throw new IndexOutOfRangeException ( $"cat_id {cat_id} is out of range" ) ;
}
}
var json = JsonContent . Create ( new
{
op = "getFeeds" ,
sid = this . SessionId ,
cat_id = cat_id ,
unread_only = unread_only ,
limit = limit ,
offset = offset
} ) ;
var response = await ( await httpClient . PostAsync ( BaseURI , json ) ) . Content . ReadAsStringAsync ( ) ;
var apiResult = JsonConvert . DeserializeObject < ttrss . messages . FeedsResponse > ( response ) ;
return apiResult . content ;
2023-04-02 23:32:03 -04:00
}
2023-04-03 14:03:31 -04:00
///<param name="enable_nested">nested mode (return only top level)</param>
public async Task < IEnumerable < Category > > GetCategories ( bool unread_only = false , bool enable_nested = false , bool include_empty = false )
2023-04-02 23:32:03 -04:00
{
assertInitialized ( ) ;
2023-04-03 14:03:31 -04:00
var json = JsonContent . Create ( new
{
op = "getCategories" ,
sid = this . SessionId ,
unread_only = unread_only ,
enable_nested = enable_nested ,
include_empty = include_empty
} ) ;
var response = await ( await httpClient . PostAsync ( BaseURI , json ) ) . Content . ReadAsStringAsync ( ) ;
var apiResult = JsonConvert . DeserializeObject < ttrss . messages . CategoriesResponse > ( response ) ;
return apiResult . content ;
2023-04-02 23:32:03 -04:00
}
public enum VIEWMODE { All , Unread , Adaptive , Marked , Updated }
public enum SORTORDER { Default , OldestFirst , NewestFirst }
2023-04-03 16:37:59 -04:00
public async Task < IEnumerable < Headline > > GetHeadlines (
int feed_id ,
bool is_cat ,
int limit = 60 ,
int skip = 0 ,
/*string filter,*/
bool show_excerpt = false ,
bool show_content = false ,
VIEWMODE view_mode = VIEWMODE . All ,
bool include_attachments = false ,
int? since_id = null ,
bool include_nested = false ,
SORTORDER order_by = SORTORDER . Default ,
bool sanitize = true ,
bool force_update = false ,
bool has_sandbox = false )
2023-04-02 23:32:03 -04:00
{
2023-04-03 16:37:59 -04:00
if ( limit > 60 )
{
assertApiLevel ( 6 ) ;
}
if ( limit > 200 )
{
throw new ArgumentOutOfRangeException ( "limit" , limit , "capped at 200" ) ;
}
if ( include_nested )
{
assertApiLevel ( 4 ) ;
}
string sortOrderString = "" ;
if ( order_by ! = SORTORDER . Default )
{
assertApiLevel ( 5 ) ;
switch ( order_by )
{
case SORTORDER . OldestFirst :
sortOrderString = "date_reverse" ;
break ;
case SORTORDER . NewestFirst :
sortOrderString = "feed_dates" ;
break ;
}
}
if ( sanitize = = false )
{
//TODO: it's version 1.8.0, but no idea what version that is. I can narrow it down to 6, 7, or 8.
assertApiLevel ( 6 ) ;
}
if ( force_update )
{
assertApiLevel ( 9 ) ;
}
var json = JsonContent . Create ( new
{
op = "getHeadlines" ,
sid = this . SessionId ,
feed_id = feed_id ,
is_cat = is_cat ,
limit = limit ,
skip = skip ,
show_excerpt = show_excerpt ,
show_content = show_content ,
view_mode = view_mode . ToString ( "D" ) ,
include_attachments = include_attachments ,
since_id = since_id ,
include_nested = include_nested ,
order_by = sortOrderString ,
sanitize = sanitize ,
force_update = force_update ,
has_sandbox = has_sandbox
} ) ;
return await getHeadlines ( json ) ;
2023-04-02 23:32:03 -04:00
}
2023-04-03 16:37:59 -04:00
public async Task < IEnumerable < Headline > > GetHeadlinesTag (
string tag ,
int limit = 200 ,
int skip = 0 ,
/*string filter,*/
bool show_excerpt = false ,
bool show_content = false ,
VIEWMODE view_mode = VIEWMODE . All ,
bool include_attachments = false ,
int? since_id = null ,
bool include_nested = false ,
SORTORDER order_by = SORTORDER . Default ,
bool sanitize = true ,
bool force_update = false ,
bool has_sandbox = false
/*bool include_header = false*/ )
2023-04-02 23:32:03 -04:00
{
2023-04-03 16:37:59 -04:00
assertApiLevel ( 18 ) ;
if ( limit > 200 )
{
throw new ArgumentOutOfRangeException ( "limit" , limit , "capped at 200" ) ;
}
string sortOrderString ;
switch ( order_by )
{
case SORTORDER . OldestFirst :
sortOrderString = "date_reverse" ;
break ;
case SORTORDER . NewestFirst :
sortOrderString = "feed_dates" ;
break ;
default :
sortOrderString = "" ;
break ;
}
var json = JsonContent . Create ( new
{
op = "getHeadlines" ,
sid = this . SessionId ,
feed_id = tag ,
limit = limit ,
skip = skip ,
show_excerpt = show_excerpt ,
show_content = show_content ,
view_mode = view_mode . ToString ( "D" ) ,
include_attachments = include_attachments ,
since_id = since_id ,
include_nested = include_nested ,
order_by = sortOrderString ,
sanitize = sanitize ,
force_update = force_update ,
has_sandbox = has_sandbox
} ) ;
return await getHeadlines ( json ) ;
2023-04-02 23:32:03 -04:00
}
2023-04-03 16:37:59 -04:00
private async Task < IEnumerable < Headline > > getHeadlines ( JsonContent parameters )
2023-04-02 23:32:03 -04:00
{
2023-04-03 16:37:59 -04:00
var response = await ( await httpClient . PostAsync ( BaseURI , parameters ) ) . Content . ReadAsStringAsync ( ) ;
var apiResult = JsonConvert . DeserializeObject < ttrss . messages . HeadlinesResponse > ( response ) ;
return apiResult . content ;
2023-04-02 23:32:03 -04:00
}
2023-04-03 16:37:59 -04:00
// public async Task<HeadlinesHeaderResponse> GetHeadlinesAndHeader(
// int feed_id,
// bool is_cat,
// int limit=60,
// int skip=0,
// /*string filter,*/
// bool show_excerpt = false,
// bool show_content=false,
// VIEWMODE view_mode = VIEWMODE.All,
// bool include_attachments = false,
// int? since_id = null,
// bool include_nested = false,
// SORTORDER order_by = SORTORDER.Default,
// bool sanitize = true,
// bool force_update = false,
// bool has_sandbox = false)
// {
// assertApiLevel(12);
// if(limit > 200)
// {
// throw new ArgumentOutOfRangeException("limit", limit, "capped at 200");
// }
// string sortOrderString = "";
// if (order_by != SORTORDER.Default)
// {
// switch (order_by)
// {
// case SORTORDER.OldestFirst:
// sortOrderString = "date_reverse";
// break;
// case SORTORDER.NewestFirst:
// sortOrderString = "feed_dates";
// break;
// }
// }
// var json = JsonContent.Create(new
// {
// op = "getHeadlines",
// sid = this.SessionId,
// feed_id = feed_id,
// is_cat = is_cat,
// limit = limit,
// skip = skip,
// show_excerpt = show_excerpt,
// show_content = show_content,
// view_mode = view_mode.ToString("D"),
// include_attachments = include_attachments,
// since_id = since_id,
// include_nested = include_nested,
// order_by = sortOrderString,
// sanitize = sanitize,
// force_update = force_update,
// has_sandbox = has_sandbox,
// include_header = true
// });
// return await getHeadlinesAndHeader(json);
// }
// public async Task<HeadlinesHeaderResponse> GetHeadlinesTagAndHeader(
// string tag,
// int limit=200,
// int skip=0,
// /*string filter,*/
// bool show_excerpt = false,
// bool show_content=false,
// VIEWMODE view_mode = VIEWMODE.All,
// bool include_attachments = false,
// int? since_id = null,
// bool include_nested = false,
// SORTORDER order_by = SORTORDER.Default,
// bool sanitize = true,
// bool force_update = false,
// bool has_sandbox = false
// /*bool include_header = false*/)
// {
// assertApiLevel(18);
// if(limit > 200)
// {
// throw new ArgumentOutOfRangeException("limit", limit, "capped at 200");
// }
// string sortOrderString;
// switch(order_by)
// {
// case SORTORDER.OldestFirst:
// sortOrderString = "date_reverse";
// break;
// case SORTORDER.NewestFirst:
// sortOrderString = "feed_dates";
// break;
// default:
// sortOrderString = "";
// break;
// }
// var json = JsonContent.Create(new
// {
// op = "getHeadlines",
// sid = this.SessionId,
// feed_id = tag,
// limit = limit,
// skip = skip,
// show_excerpt = show_excerpt,
// show_content = show_content,
// view_mode = view_mode.ToString("D"),
// include_attachments = include_attachments,
// since_id = since_id,
// include_nested = include_nested,
// order_by = sortOrderString,
// sanitize = sanitize,
// force_update = force_update,
// has_sandbox = has_sandbox,
// include_header = true
// });
// return await getHeadlinesAndHeader(json);
// }
// private async Task<HeadlinesHeaderResponse> getHeadlinesAndHeader(JsonContent parameters)
// {
// var response = await (await httpClient.PostAsync(BaseURI, parameters)).Content.ReadAsStringAsync();
// var apiResult = JsonConvert.DeserializeObject<ttrss.messages.HeadlinesHeaderResponse>(response);
// return apiResult;
// }
2023-04-02 23:32:03 -04:00
public enum UPDATEMODE { SetFalse , SetTrue , Toggle }
public enum UPDATEFIELD { starred , published , unread }
public async Task < int > UpdateArticleField ( UPDATEMODE mode , UPDATEFIELD field , params int [ ] ids )
{
//documentation: UpdateArticle - for note, we have a separate method UpdateArticleNote
assertInitialized ( ) ;
throw new NotImplementedException ( ) ;
}
public async Task < int > UpdateArticleNote ( string data , params int [ ] ids )
{
//documentation: UpdateArticle - for fields other than note, we have a separate method UpdateArticleField
assertInitialized ( ) ;
throw new NotImplementedException ( ) ;
}
public async Task < IEnumerable < object > > GetArticle ( params int [ ] article_id )
{
if ( ! article_id . Any ( ) )
{
throw new ArgumentException ( "need at least one article_id" ) ;
}
assertInitialized ( ) ;
throw new NotImplementedException ( ) ;
}
public async Task < Configuration > GetConfig ( )
{
assertInitialized ( ) ;
throw new NotImplementedException ( ) ;
}
///<summary>
///tell the feed to update. As opposed to updating our configuration of the feed.
///</summary>
public async Task UpdateFeed ( int feed_id )
{
assertInitialized ( ) ;
throw new NotImplementedException ( ) ;
}
public async Task < string > GetPref ( string key )
{
assertInitialized ( ) ;
throw new NotImplementedException ( ) ;
}
public enum CATCHUPMODE { All , OneDay , OneWeek , TwoWeeks }
public async Task CatchupFeed ( int feed_id , bool is_cat , CATCHUPMODE mode = CATCHUPMODE . All )
{
assertInitialized ( ) ;
throw new NotImplementedException ( ) ;
}
public async Task GetLabels ( int? article_id )
{
assertInitialized ( ) ;
2023-04-03 00:36:53 -04:00
if ( article_id ! = null )
2023-04-02 23:32:03 -04:00
{
assertApiLevel ( 5 ) ;
}
throw new NotImplementedException ( ) ;
}
public async Task SetArticleLabel ( int label_id , bool assign , params int [ ] article_ids )
{
//there's a label "cache", i guess?
assertInitialized ( ) ;
throw new NotImplementedException ( ) ;
}
public async Task ShareToPublished ( string title , Uri url , string content , bool sanitize = true )
{
assertInitialized ( ) ;
assertApiLevel ( 4 ) ;
2023-04-03 00:36:53 -04:00
if ( ! sanitize )
2023-04-02 23:32:03 -04:00
assertApiLevel ( 20 ) ;
throw new NotImplementedException ( ) ;
}
///<summary>
///<param name="login"/>if the feed requires basic HTTP auth; the login. (decidedly not self-explanatory, gdi)</param>
///<param name="password"/>if the feed requires basic HTTP auth; the password. (decidedly not self-explanatory, gdi)</param>
///</summary>
public async Task SubscribeToFeed ( Uri feed_url , int category_id , string login , string password )
{
assertInitialized ( ) ;
assertApiLevel ( 5 ) ;
throw new NotImplementedException ( ) ;
}
public async Task UnsubscribeFeed ( )
{
assertInitialized ( ) ;
assertApiLevel ( 5 ) ;
throw new NotImplementedException ( ) ;
}
public async Task GetFeedTree ( )
{
assertInitialized ( ) ;
assertApiLevel ( 5 ) ;
throw new NotImplementedException ( ) ;
}
private void assertInitialized ( )
{
if ( SessionId = = null )
{
throw new InvalidOperationException ( "no session ID - call Login first!" ) ;
}
}
private void assertApiLevel ( int ApiLevel )
{
2023-04-03 00:36:53 -04:00
if ( ApiLevel > this . api_level )
2023-04-02 23:32:03 -04:00
{
throw new NotSupportedException ( $"method requires api level {ApiLevel}, have {this.api_level}" ) ;
}
}
2023-04-03 00:36:53 -04:00
private async Task < T > oneValueGet < T > ( string op , string key )
{
2023-04-03 13:09:39 -04:00
//mostly you post {"op": "getAThing", "sid": "sessionId"}
//and get back something like {"seq": 0, "status": 0, "content": {"the value you asked for": 0}}
2023-04-03 00:36:53 -04:00
var json = JsonContent . Create ( new
{
op = op ,
sid = this . SessionId
} ) ;
var response = await ( await httpClient . PostAsync ( BaseURI , json ) ) . Content . ReadAsStringAsync ( ) ;
var apiResult = JsonConvert . DeserializeObject < ttrss . messages . GenericApiResponse > ( response ) ;
try
{
var converter = TypeDescriptor . GetConverter ( typeof ( T ) ) ;
if ( converter ! = null )
{
return ( T ) converter . ConvertFromString ( apiResult . Content [ key ] ) ;
}
return default ( T ) ;
}
catch ( NotSupportedException )
{
return default ( T ) ;
}
}
2023-04-02 23:32:03 -04:00
}
}