Compare commits

...

4 Commits

Author SHA1 Message Date
794d03b424 hey adam... y'all ever heard of a generic object? 2023-04-03 16:47:24 -04:00
e4cbf5e7d4 regular get headlines 2023-04-03 16:37:59 -04:00
a5eea59678 get categories 2023-04-03 14:03:31 -04:00
7f4fdd22b6 Get Feeds 2023-04-03 13:50:33 -04:00
11 changed files with 452 additions and 50 deletions

View File

@ -1,4 +1,5 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Linq;
namespace ttrss_co_client namespace ttrss_co_client
{ {
@ -16,8 +17,17 @@ namespace ttrss_co_client
var apiLevel = await ttrssClient.GetApiLevel(); var apiLevel = await ttrssClient.GetApiLevel();
Console.WriteLine($"api level: {apiLevel}"); Console.WriteLine($"api level: {apiLevel}");
var counters = await ttrssClient.GetCounters(true, false, false, false); var cats = await ttrssClient.GetCategories(include_empty: true);
Console.WriteLine($"{counters.Count()} counter{(counters.Count() == 1 ? "" : "s")} found"); Console.WriteLine($"{cats.Count()} categor{(cats.Count() == 1 ? "y" : "ies")} found");
var firstUnread = cats.FirstOrDefault(f => f.unread > 0);
if(firstUnread != null)
{
Console.WriteLine($"first unread: {firstUnread.title} (id {firstUnread.id})");
}
else
{
Console.WriteLine("no categories with feeds with unread articles.");
}
var loggedout = await ttrssClient.Logout(); var loggedout = await ttrssClient.Logout();
Console.WriteLine($"logged out: {loggedout}"); Console.WriteLine($"logged out: {loggedout}");

View File

@ -2,9 +2,11 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Net.Http.Json; using System.Net.Http.Json;
using ttrss_co_client.ttrss.messages;
using ttrss_co_client.ttrss.datastructures; using ttrss_co_client.ttrss.datastructures;
// https://tt-rss.org/wiki/ApiReference // https://tt-rss.org/wiki/ApiReference
//TODO: a lot of these are a given at API level 1, so double check all necessary API levels
namespace ttrss_co_client.ttrss namespace ttrss_co_client.ttrss
{ {
@ -31,11 +33,19 @@ namespace ttrss_co_client.ttrss
}; };
var content = JsonContent.Create(json); var content = JsonContent.Create(json);
var response = await (await httpClient.PostAsync(BaseURI, content)).Content.ReadAsStringAsync(); var response = await (await httpClient.PostAsync(BaseURI, content)).Content.ReadAsStringAsync();
var loginResult = JsonConvert.DeserializeObject<ttrss.messages.LoginResponse>(response); var loginResult = JsonConvert.DeserializeObject<ApiResponse<LoginResponseContent>>(response);
if (loginResult.status == 0) if (loginResult.status == 0)
{ {
SessionId = loginResult.content.session_id; SessionId = loginResult.Content.session_id;
api_level = loginResult.content.api_level ?? 1; 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;
}
Console.WriteLine(SessionId); Console.WriteLine(SessionId);
} }
else else
@ -46,6 +56,16 @@ namespace ttrss_co_client.ttrss
public async Task<int> GetApiLevel() public async Task<int> GetApiLevel()
{ {
//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
assertInitialized(); assertInitialized();
return await oneValueGet<int>("getApiLevel", "level"); return await oneValueGet<int>("getApiLevel", "level");
} }
@ -71,7 +91,6 @@ namespace ttrss_co_client.ttrss
public async Task<IEnumerable<CounterInfo>> GetCounters(bool feeds = true, bool labels = true, bool categories = true, bool tags = false) public async Task<IEnumerable<CounterInfo>> GetCounters(bool feeds = true, bool labels = true, bool categories = true, bool tags = false)
{ {
assertInitialized(); assertInitialized();
assertApiLevel(4);
var output_mode = ""; var output_mode = "";
if (feeds) output_mode += "f"; if (feeds) output_mode += "f";
@ -86,34 +105,325 @@ namespace ttrss_co_client.ttrss
output_mode = output_mode output_mode = output_mode
}); });
var response = await (await httpClient.PostAsync(BaseURI, json)).Content.ReadAsStringAsync(); var response = await (await httpClient.PostAsync(BaseURI, json)).Content.ReadAsStringAsync();
var apiResult = JsonConvert.DeserializeObject<ttrss.messages.CounterInfoResponse>(response); var apiResult = JsonConvert.DeserializeObject<ApiResponse<IEnumerable<CounterInfo>>>(response);
return apiResult.content; return apiResult.Content;
} }
public async Task GetFeeds(int cat_id, bool unread_only, int limit, int offset, bool include_nested) ///<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*/)
{ {
assertInitialized(); assertInitialized();
throw new NotImplementedException(); 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<ApiResponse<IEnumerable<Feed>>>(response);
return apiResult.Content;
} }
public async Task GetCategories(bool unread_only, bool enable_nested, bool include_empty) ///<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)
{ {
assertInitialized(); assertInitialized();
throw new NotImplementedException();
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<ApiResponse<IEnumerable<Category>>>(response);
return apiResult.Content;
} }
public enum VIEWMODE { All, Unread, Adaptive, Marked, Updated } public enum VIEWMODE { All, Unread, Adaptive, Marked, Updated }
public enum SORTORDER { Default, OldestFirst, NewestFirst } public enum SORTORDER { Default, OldestFirst, NewestFirst }
public async Task GetHeadlines(int feed_id, int limit, int skip, /*string filter, */ bool is_cat, bool show_excerpt, bool show_content, VIEWMODE view_mode, bool include_attachments, int since_id, bool include_nested, SORTORDER order_by, bool sanitize, bool force_update = false, bool has_sandbox = false, bool include_header = false) 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)
{ {
await getHeadlines(feed_id, null, limit, skip, /*filter, */ is_cat, show_excerpt, show_content, view_mode, include_attachments, since_id, include_nested, order_by, sanitize, force_update, has_sandbox, include_header); 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);
} }
public async Task GetHeadlines(string feed_id, int limit, int skip, /*string filter, */ bool is_cat, bool show_excerpt, bool show_content, VIEWMODE view_mode, bool include_attachments, int since_id, bool include_nested, SORTORDER order_by, bool sanitize, bool force_update = false, bool has_sandbox = false, bool include_header = false) 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*/)
{ {
await getHeadlines(null, feed_id, limit, skip, /*filter, */ is_cat, show_excerpt, show_content, view_mode, include_attachments, since_id, include_nested, order_by, sanitize, force_update, has_sandbox, include_header); 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);
} }
private async Task getHeadlines(int? int_feed_id, string string_feed_id, int limit, int skip, /*string filter, */ bool is_cat, bool show_excerpt, bool show_content, VIEWMODE view_mode, bool include_attachments, int since_id, bool include_nested, SORTORDER order_by, bool sanitize, bool force_update, bool has_sandbox, bool include_header) private async Task<IEnumerable<Headline>> getHeadlines(JsonContent parameters)
{ {
assertInitialized(); var response = await (await httpClient.PostAsync(BaseURI, parameters)).Content.ReadAsStringAsync();
throw new NotImplementedException(); var apiResult = JsonConvert.DeserializeObject<ApiResponse<IEnumerable<Headline>>>(response);
return apiResult.Content;
} }
// 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;
// }
public enum UPDATEMODE { SetFalse, SetTrue, Toggle } public enum UPDATEMODE { SetFalse, SetTrue, Toggle }
public enum UPDATEFIELD { starred, published, unread } public enum UPDATEFIELD { starred, published, unread }
public async Task<int> UpdateArticleField(UPDATEMODE mode, UPDATEFIELD field, params int[] ids) public async Task<int> UpdateArticleField(UPDATEMODE mode, UPDATEFIELD field, params int[] ids)
@ -232,7 +542,7 @@ namespace ttrss_co_client.ttrss
sid = this.SessionId sid = this.SessionId
}); });
var response = await (await httpClient.PostAsync(BaseURI, json)).Content.ReadAsStringAsync(); var response = await (await httpClient.PostAsync(BaseURI, json)).Content.ReadAsStringAsync();
var apiResult = JsonConvert.DeserializeObject<ttrss.messages.GenericApiResponse>(response); var apiResult = JsonConvert.DeserializeObject<ttrss.messages.ApiResponse<Dictionary<string, string>>>(response);
try try
{ {
var converter = TypeDescriptor.GetConverter(typeof(T)); var converter = TypeDescriptor.GetConverter(typeof(T));

View File

@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace ttrss_co_client.ttrss.datastructures
{
public class Category
{
public int id { get; set; }
public string title { get; set; }
public int unread { get; set; }
public int order_id { get; set; }
}
}

View File

@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace ttrss_co_client.ttrss.datastructures
{
public class Feed
{
public int id { get; set; }
public Uri feed_url { get; set; }
public string title { get; set; }
public int unread { get; set; }
public bool has_icon { get; set; }
public int cat_id { get; set; }
///<summary>unix timestamp, see <see cref="TimeStamp" /></summary>
public int last_updated { get; set; }
public DateTime? TimeStamp
{
get
{
return new DateTime(1970,1,1,0,0,0,0,System.DateTimeKind.Utc)
.AddSeconds( last_updated ).ToLocalTime();
}
}
public int order_id { get; set; }
}
}

View File

@ -0,0 +1,32 @@
using System.Collections.Generic;
namespace ttrss_co_client.ttrss.datastructures
{
public class Headline
{
public int id { get; set; }
public Guid guid { get; set; }
public bool unread { get; set; }
public bool marked { get; set; }
public bool published { get; set; }
public int updated { get; set; }
public bool is_updated { get; set; }
public string title { get; set; }
public Uri link { get; set; }
public int feed_id { get; set; }
public IEnumerable<string> tags { get; set; }
public string content { get; set; }
///See <cref name="ttrss_co_client.ttrss.datastructures.Label" />
public IEnumerable<IEnumerable<string>> labels { get; set; }
public string feed_title { get; set; }
public int comments_count { get; set; }
public Uri comments_link { get; set; }
public bool always_display_attachments { get; set; }
public string author { get; set; }
public double score { get; set; }
public string note { get; set; }
public string lang { get; set; }
public Uri flavor_image { get; set; }
public string flavor_stream { get; set; }
}
}

View File

@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace ttrss_co_client.ttrss.datastructures
{
public class Label
{
public Label(string[] fromServer)
{
int.TryParse(fromServer[0], out id);
Title = fromServer[1];
unknown = fromServer[2];
Color = System.Drawing.ColorTranslator.FromHtml(fromServer[3]);
}
public int id;
public string Title { get; set; }
public string unknown { get; set; }
public System.Drawing.Color Color {get; set;}
}
}

View File

@ -2,13 +2,10 @@ using System.Collections.Generic;
namespace ttrss_co_client.ttrss.messages namespace ttrss_co_client.ttrss.messages
{ {
public abstract class ApiResponse public class ApiResponse<T>
{ {
public int seq { get; set; } public int seq { get; set; }
public int status { get; set; } public int status { get; set; }
} public T Content {get; set;}
public class GenericApiResponse : ApiResponse
{
public Dictionary<string, string> Content{get; set;}
} }
} }

View File

@ -1,9 +0,0 @@
using ttrss_co_client.ttrss.datastructures;
namespace ttrss_co_client.ttrss.messages
{
public class CounterInfoResponse : ApiResponse
{
public IEnumerable<CounterInfo> content { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using ttrss_co_client.ttrss.datastructures;
namespace ttrss_co_client.ttrss.messages
{
public class HeadlinesHeaderContent
{
public int id { get; set; }
public int first_id { get; set; }
public bool is_cat { get; set; }
}
}

View File

@ -1,15 +0,0 @@
namespace ttrss_co_client.ttrss.messages
{
public class LoginResponse : ApiResponse
{
public Content content { get; set; }
public class Content
{
public string session_id { get; set; }
public Configuration config { get; set; }
public int? api_level { get; set; }
public string error { get; set; }
}
}
}

View File

@ -0,0 +1,10 @@
namespace ttrss_co_client.ttrss.messages
{
public class LoginResponseContent
{
public string session_id { get; set; }
public Configuration config { get; set; }
public int? api_level { get; set; }
public string error { get; set; }
}
}