Compare commits

...

10 Commits

Author SHA1 Message Date
3936850913 get tree 2023-04-04 17:31:51 -04:00
c53752d305 unsub from feed 2023-04-04 15:01:50 -04:00
bbf090eae9 subscribe to feed - seems to work 2023-04-04 14:50:30 -04:00
f224aa8506 apply label 2023-04-04 13:52:10 -04:00
60eb4a61b9 get lables 2023-04-04 13:36:08 -04:00
5d2a4e28fa catchup feed 2023-04-04 13:12:08 -04:00
6d63d51a57 udpate feed and get pref
i think. i don't know any prefs to check.
2023-04-04 12:46:30 -04:00
ff232db4bc getCOnfig 2023-04-04 12:13:12 -04:00
282c05eab5 configuration is a data structure 2023-04-04 12:10:01 -04:00
7c7a2c0ae7 getArticle 2023-04-04 12:08:48 -04:00
8 changed files with 327 additions and 72 deletions

View File

@ -37,7 +37,7 @@ namespace ttrss_co_client.ttrss
if (loginResult.status == 0) if (loginResult.status == 0)
{ {
SessionId = loginResult.Content.session_id; SessionId = loginResult.Content.session_id;
if(loginResult.Content.api_level == null) 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?)"); 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?)");
} }
@ -66,25 +66,26 @@ namespace ttrss_co_client.ttrss
//???? = 7 //???? = 7
//???? = 8 //???? = 8
//1.14 = 9 //1.14 = 9
//1.15 = 10
assertInitialized(); assertInitialized();
return await oneValueGet<int>("getApiLevel", "level"); return await getOneValue<int>("getApiLevel", "level");
} }
public async Task<bool> Logout() public async Task<bool> Logout()
{ {
assertInitialized(); assertInitialized();
return (await oneValueGet<string>("logout", "status"))?.ToLower() == "ok"; return (await getOneValue<string>("logout", "status"))?.ToLower() == "ok";
} }
public async Task<bool> IsLoggedIn() public async Task<bool> IsLoggedIn()
{ {
//assertInitialized(); //assertInitialized();
return (await oneValueGet<bool>("isLoggedIn", "status")); return (await getOneValue<bool>("isLoggedIn", "status"));
} }
public async Task<int> GetUnread() public async Task<int> GetUnread()
{ {
assertInitialized(); assertInitialized();
return await oneValueGet<int>("getUnread", "unread"); return await getOneValue<int>("getUnread", "unread");
} }
///<summary>at least in my installation, it doesn't seem to respect the parameters I give it, be it curl or here.</summary> ///<summary>at least in my installation, it doesn't seem to respect the parameters I give it, be it curl or here.</summary>
@ -111,12 +112,12 @@ namespace ttrss_co_client.ttrss
///<param name="limit">0 for all</param> ///<param name="limit">0 for all</param>
///<param name="offset">skip this amount first</param> ///<param name="offset">skip this amount first</param>
///<param name="include_nested">idk, doesn't affect what the documentation says it should</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*/) 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();
if(cat_id <-2) if (cat_id < -2)
{ {
if(cat_id == -3 || cat_id == -4) if (cat_id == -3 || cat_id == -4)
{ {
assertApiLevel(4); assertApiLevel(4);
} }
@ -161,11 +162,11 @@ namespace ttrss_co_client.ttrss
public async Task<IEnumerable<Headline>> GetHeadlines( public async Task<IEnumerable<Headline>> GetHeadlines(
int feed_id, int feed_id,
bool is_cat, bool is_cat,
int limit=60, int limit = 60,
int skip=0, int skip = 0,
/*string filter,*/ /*string filter,*/
bool show_excerpt = false, bool show_excerpt = false,
bool show_content=false, bool show_content = false,
VIEWMODE view_mode = VIEWMODE.All, VIEWMODE view_mode = VIEWMODE.All,
bool include_attachments = false, bool include_attachments = false,
int? since_id = null, int? since_id = null,
@ -175,16 +176,16 @@ namespace ttrss_co_client.ttrss
bool force_update = false, bool force_update = false,
bool has_sandbox = false) bool has_sandbox = false)
{ {
if(limit>60) if (limit > 60)
{ {
assertApiLevel(6); assertApiLevel(6);
} }
if(limit > 200) if (limit > 200)
{ {
throw new ArgumentOutOfRangeException("limit", limit, "capped at 200"); throw new ArgumentOutOfRangeException("limit", limit, "capped at 200");
} }
if(include_nested) if (include_nested)
{ {
assertApiLevel(4); assertApiLevel(4);
} }
@ -204,12 +205,12 @@ namespace ttrss_co_client.ttrss
} }
} }
if(sanitize == false) 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. //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); assertApiLevel(6);
} }
if(force_update) if (force_update)
{ {
assertApiLevel(9); assertApiLevel(9);
} }
@ -237,11 +238,11 @@ namespace ttrss_co_client.ttrss
} }
public async Task<IEnumerable<Headline>> GetHeadlinesTag( public async Task<IEnumerable<Headline>> GetHeadlinesTag(
string tag, string tag,
int limit=200, int limit = 200,
int skip=0, int skip = 0,
/*string filter,*/ /*string filter,*/
bool show_excerpt = false, bool show_excerpt = false,
bool show_content=false, bool show_content = false,
VIEWMODE view_mode = VIEWMODE.All, VIEWMODE view_mode = VIEWMODE.All,
bool include_attachments = false, bool include_attachments = false,
int? since_id = null, int? since_id = null,
@ -254,13 +255,13 @@ namespace ttrss_co_client.ttrss
{ {
assertApiLevel(18); assertApiLevel(18);
if(limit > 200) if (limit > 200)
{ {
throw new ArgumentOutOfRangeException("limit", limit, "capped at 200"); throw new ArgumentOutOfRangeException("limit", limit, "capped at 200");
} }
string sortOrderString; string sortOrderString;
switch(order_by) switch (order_by)
{ {
case SORTORDER.OldestFirst: case SORTORDER.OldestFirst:
sortOrderString = "date_reverse"; sortOrderString = "date_reverse";
@ -428,12 +429,12 @@ namespace ttrss_co_client.ttrss
// return apiResult; // return apiResult;
// } // }
#endregion #endregion
public enum UPDATEMODE { SetFalse=0, SetTrue=1, Toggle=2 }
///<summary>to update note, see <see cref="UpdateArticleNote"/></summary> ///<summary>to update note, see <see cref="UpdateArticleNote"/></summary>
public enum UPDATEFIELD { starred=0, published=1, unread=2 } public enum UPDATEFIELD { starred = 0, published = 1, unread = 2 }
public async Task<int> UpdateArticleField(UPDATEMODE mode, UPDATEFIELD field, params int[] ids) public enum UPDATEMODE { SetFalse = 0, SetTrue = 1, Toggle = 2 }
public async Task<int> UpdateArticleField(UPDATEFIELD field, UPDATEMODE mode, params int[] ids)
{ {
if(ids == null || ids.Length == 0) if (ids == null || ids.Length == 0)
{ {
throw new System.ArgumentNullException("ids", "need to specify at least one id"); throw new System.ArgumentNullException("ids", "need to specify at least one id");
} }
@ -457,11 +458,11 @@ namespace ttrss_co_client.ttrss
///<summary>for fields other than note, we have a separate method UpdateArticleField</summary> ///<summary>for fields other than note, we have a separate method UpdateArticleField</summary>
public async Task<int> UpdateArticleNote(string data, params int[] ids) public async Task<int> UpdateArticleNote(string data, params int[] ids)
{ {
if(ids == null || ids.Length == 0) if (ids == null || ids.Length == 0)
{ {
throw new System.ArgumentNullException("ids", "need to specify at least one id"); throw new System.ArgumentNullException("ids", "need to specify at least one id");
} }
//documentation: UpdateArticle - for note, we have a separate method UpdateArticleNote
assertInitialized(); assertInitialized();
var json = JsonContent.Create(new var json = JsonContent.Create(new
@ -476,83 +477,251 @@ namespace ttrss_co_client.ttrss
var apiResult = JsonConvert.DeserializeObject<ApiResponse<ArticleUpdateFieldResponse>>(response); var apiResult = JsonConvert.DeserializeObject<ApiResponse<ArticleUpdateFieldResponse>>(response);
return apiResult.Content.updated; return apiResult.Content.updated;
} }
public async Task<IEnumerable<object>> GetArticle(params int[] article_id) public async Task<IEnumerable<Article>> GetArticles(params int[] article_id)
{ {
if (!article_id.Any()) if (!article_id.Any())
{ {
throw new ArgumentException("need at least one article_id"); throw new ArgumentException("need at least one article_id");
} }
assertInitialized(); assertInitialized();
throw new NotImplementedException();
var json = JsonContent.Create(new
{
op = "getArticle",
sid = this.SessionId,
article_id = string.Join(',', article_id)
});
return await get<IEnumerable<Article>>(json);
} }
public async Task<Configuration> GetConfig() public async Task<datastructures.Configuration> GetConfig()
{ {
assertInitialized(); assertInitialized();
throw new NotImplementedException(); var json = JsonContent.Create(new
{
op = "getConfig",
sid = this.SessionId
});
return await get<datastructures.Configuration>(json);
} }
///<summary> ///<summary>
///tell the feed to update. As opposed to updating our configuration of the feed. ///tell the feed to update. As opposed to updating our configuration of the feed.
///</summary> ///</summary>
public async Task UpdateFeed(int feed_id) public async Task<bool> UpdateFeed(int feed_id)
{ {
assertInitialized(); assertInitialized();
throw new NotImplementedException();
var json = JsonContent.Create(new
{
op = "updateFeed",
sid = this.SessionId,
feed_id = feed_id
});
var apiResponse = await get<Dictionary<string, string>>(json);
return apiResponse.ContainsKey("status") && apiResponse["status"]?.ToLower() == "ok";
} }
public async Task<string> GetPref(string key) public async Task<T> GetPref<T>(string pref)
{ {
assertInitialized(); assertInitialized();
throw new NotImplementedException();
var json = JsonContent.Create(new
{
op = "getPref",
sid = this.SessionId,
pref_name = pref
});
var apiResponse = await get<Dictionary<string, string>>(json);
try
{
var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
return (T)converter.ConvertFromString(apiResponse["value"]);
}
return default(T);
}
catch (NotSupportedException)
{
return default(T);
}
} }
public enum CATCHUPMODE { All, OneDay, OneWeek, TwoWeeks } public enum CATCHUPMODE { All, OneDay, OneWeek, TwoWeeks }
public async Task CatchupFeed(int feed_id, bool is_cat, CATCHUPMODE mode = CATCHUPMODE.All) ///<summary>Tries to catchup (e.g. mark as read) specified feed.</summary>
public async Task<bool> CatchupFeed(int feed_id, bool is_cat, CATCHUPMODE mode = CATCHUPMODE.All)
{ {
assertInitialized(); assertInitialized();
throw new NotImplementedException();
var modestring = "all";
if (mode != CATCHUPMODE.All)
{
assertApiLevel(15);
switch (mode)
{
case CATCHUPMODE.OneDay:
modestring = "1day";
break;
case CATCHUPMODE.OneWeek:
modestring = "1week";
break;
case CATCHUPMODE.TwoWeeks:
modestring = "2week";
break;
} }
public async Task GetLabels(int? article_id) }
var json = JsonContent.Create(new
{
op = "catchupFeed",
sid = this.SessionId,
feed_id = feed_id,
is_cat = is_cat,
mode = modestring
});
var apiResponse = await get<Dictionary<string, string>>(json);
return apiResponse.ContainsKey("status") && apiResponse["status"]?.ToLower() == "ok";
}
public async Task<IEnumerable<Label>> GetLabels(int? article_id = null)
{ {
assertInitialized(); assertInitialized();
if (article_id != null)
var json = JsonContent.Create(new
{ {
assertApiLevel(5); op = "getLabels",
} sid = this.SessionId,
throw new NotImplementedException(); article_id = article_id
} });
public async Task SetArticleLabel(int label_id, bool assign, params int[] article_ids) var labels = await get<IEnumerable<Label>>(json);
if (this.api_level < 5)
{ {
//there's a label "cache", i guess? foreach (var l in labels)
{
l.id = -11 - l.id;
}
}
return labels;
}
public async Task<int> SetArticleLabel(int label_id, bool assign, params int[] article_ids)
{
if (article_ids == null || article_ids.Length == 0)
{
throw new System.ArgumentNullException("ids", "need to specify at least one id");
}
assertInitialized(); assertInitialized();
throw new NotImplementedException();
} var json = JsonContent.Create(new
public async Task ShareToPublished(string title, Uri url, string content, bool sanitize = true)
{ {
op = "setArticleLabel",
sid = this.SessionId,
article_ids = string.Join(',', article_ids),
label_id = label_id,
assign = assign
});
var apiResponse = await get<Dictionary<string, string>>(json);
if (!apiResponse.ContainsKey("status") && apiResponse["status"]?.ToLower() == "ok")
{
return 0;
}
if (this.api_level <= 10)
{
//update label cache
var tasks = new List<Task>();
foreach (var id in article_ids)
{
tasks.Add(GetLabels(id));
}
Task.WaitAll(tasks.ToArray());
}
int toReturn;
if (int.TryParse(apiResponse["updated"], out toReturn))
{
return toReturn;
}
else
{
throw new Exception("update ostensibly ok, but couldn't parse");
}
}
public async Task<bool> ShareToPublished(string title, Uri url, string content, bool sanitize = true)
{
if (url == null)
{
throw new ArgumentNullException("url");
}
assertInitialized(); assertInitialized();
assertApiLevel(4); assertApiLevel(4);
if (!sanitize) if (!sanitize)
assertApiLevel(20); assertApiLevel(20);
throw new NotImplementedException();
var json = JsonContent.Create(new
{
op = "shareToPublished",
sid = this.SessionId,
title = title,
url = url.ToString(),
content = content,
sanitize = sanitize
});
var apiResponse = await get<Dictionary<string, string>>(json);
return apiResponse.ContainsKey("status") && apiResponse["status"]?.ToLower() == "ok";
} }
///<summary> ///<param name="category_id">0 = uncategorized</param>
///<param name="login"/>if the feed requires basic HTTP auth; the login. (decidedly not self-explanatory, gdi)</param> ///<param name="login">probably 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> ///<param name="password">probably if the feed requires basic HTTP auth; the password? (decidedly not self-explanatory, gdi)</param>
///</summary> ///<returns>¯\_(ツ)_/¯ - code 1 = success, I think? - the documentation says "See subscribe_to_feed() in functions.php for details" - that function doesn't exist (as of 2023-04-04).</returns>
public async Task SubscribeToFeed(Uri feed_url, int category_id, string login, string password) public async Task<int> SubscribeToFeed(Uri feed_url, int category_id = 0, string login = null, string password = null)
{ {
assertInitialized(); assertInitialized();
assertApiLevel(5); assertApiLevel(5);
throw new NotImplementedException();
var json = JsonContent.Create(new
{
op = "subscribeToFeed",
sid = this.SessionId,
feed_url = feed_url.ToString(),
category_id = category_id,
login = login,
password = password
});
var apiResponse = await get<Dictionary<string, Dictionary<string, int>>>(json);
return apiResponse["status"]["code"];
} }
public async Task UnsubscribeFeed() public async Task<bool> UnsubscribeFeed(int feed_id)
{ {
assertInitialized(); assertInitialized();
assertApiLevel(5); assertApiLevel(5);
throw new NotImplementedException();
var json = JsonContent.Create(new
{
op = "unsubscribeFeed",
sid = this.SessionId,
feed_id = feed_id.ToString()
});
var apiResponse = await get<Dictionary<string, string>>(json);
return apiResponse.ContainsKey("status") && apiResponse["status"]?.ToLower() == "ok";
} }
public async Task GetFeedTree() ///<returns>(almost) the whole shebang, for a user</returns>
public async Task<Tree> GetFeedTree(bool include_empty = false)
{ {
assertInitialized(); assertInitialized();
assertApiLevel(5); assertApiLevel(5);
throw new NotImplementedException();
var json = JsonContent.Create(new
{
op = "getFeedTree",
sid = this.SessionId,
include_empty = include_empty
});
return await get<Tree>(json);
} }
private void assertInitialized() private void assertInitialized()
{ {
@ -570,7 +739,7 @@ namespace ttrss_co_client.ttrss
} }
private async Task<T> oneValueGet<T>(string op, string key) private async Task<T> getOneValue<T>(string op, string key)
{ {
//mostly you post {"op": "getAThing", "sid": "sessionId"} //mostly you post {"op": "getAThing", "sid": "sessionId"}
//and get back something like {"seq": 0, "status": 0, "content": {"the value you asked for": 0}} //and get back something like {"seq": 0, "status": 0, "content": {"the value you asked for": 0}}
@ -595,5 +764,11 @@ namespace ttrss_co_client.ttrss
return default(T); return default(T);
} }
} }
private async Task<T> get<T>(JsonContent json)
{
var response = await (await httpClient.PostAsync(BaseURI, json)).Content.ReadAsStringAsync();
var apiResult = JsonConvert.DeserializeObject<ttrss.messages.ApiResponse<T>>(response);
return apiResult.Content;
}
} }
} }

View File

@ -0,0 +1,28 @@
using System.Collections.Generic;
namespace ttrss_co_client.ttrss.datastructures
{
public class Article
{
public int id { get; set; }
//TODO: custom converter for ttrss's guid
public string guid { get; set; }
public string title;
public Uri link;
//label-like. seems to be id, caption, fg color, bg color.
public IEnumerable<IEnumerable<string>> labels;
public bool unread;
public bool marked;
public bool published;
public string comments;
public string author;
public int updated;
public int feed_id;
public IEnumerable<Attachment> attachments;
public double score;
public string feed_title;
public string note;
public string lang;
public string content;
}
}

View File

@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace ttrss_co_client.ttrss.datastructures
{
public class Attachment
{
public int id { get; set; }
public Uri content_url { get; set; }
public string content_type { get; set; }
///<summary>a.k.a. article id</summary>
public int post_id { get; set; }
public string title { get; set; }
public string duration { get; set; }
public int width { get; set; }
public int height { get; set; }
}
}

View File

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http.Json; using System.Net.Http.Json;
namespace ttrss_co_client.ttrss namespace ttrss_co_client.ttrss.datastructures
{ {
public class Configuration public class Configuration
{ {

View File

@ -10,6 +10,7 @@ namespace ttrss_co_client.ttrss.datastructures
public int unread { get; set; } public int unread { get; set; }
public bool has_icon { get; set; } public bool has_icon { get; set; }
public int cat_id { get; set; } public int cat_id { get; set; }
//TODO: custom converter for unix timestamps
///<summary>unix timestamp, see <see cref="TimeStamp" /></summary> ///<summary>unix timestamp, see <see cref="TimeStamp" /></summary>
public int last_updated { get; set; } public int last_updated { get; set; }
public DateTime? TimeStamp public DateTime? TimeStamp

View File

@ -5,7 +5,8 @@ namespace ttrss_co_client.ttrss.datastructures
public class Headline public class Headline
{ {
public int id { get; set; } public int id { get; set; }
public Guid guid { get; set; } //TODO: custom converter for ttrss's guid
public string guid { get; set; }
public bool unread { get; set; } public bool unread { get; set; }
public bool marked { get; set; } public bool marked { get; set; }
public bool published { get; set; } public bool published { get; set; }

View File

@ -4,16 +4,17 @@ namespace ttrss_co_client.ttrss.datastructures
{ {
public class Label public class Label
{ {
public Label(string[] fromServer) public void FromArticle(string[] fromServer)
{ {
int.TryParse(fromServer[0], out id); int.TryParse(fromServer[0], out id);
Title = fromServer[1]; caption = fromServer[1];
unknown = fromServer[2]; fg_color = System.Drawing.ColorTranslator.FromHtml(fromServer[2]);
Color = System.Drawing.ColorTranslator.FromHtml(fromServer[3]); bg_color = System.Drawing.ColorTranslator.FromHtml(fromServer[3]);
} }
public int id; public int id;
public string Title { get; set; } public string caption { get; set; }
public string unknown { get; set; } public System.Drawing.Color fg_color { get; set; }
public System.Drawing.Color Color {get; set;} public System.Drawing.Color bg_color { get; set; }
} public bool @checked { get; set;}
}
} }

View File

@ -0,0 +1,32 @@
using System.Collections.Generic;
namespace ttrss_co_client.ttrss.datastructures
{
public class Tree
{
public ItemCollection categories { get; set; }
public class ItemCollection
{
public string identifier { get; set; }
public string label { get; set; }
public IEnumerable<Item> items { get; set; }
public class Item
{
public string id { get; set; }
public int? bare_id { get; set; }
public int? auxcounter { get; set; }
public string name { get; set; }
public IEnumerable<Item> items { get; set; }
public bool? checkbox { get; set; }
public string type { get; set; }
public int? unread { get; set; }
public int? child_unread { get; set; }
public string param { get; set; }
public string fg_color { get; set; }
public string bg_color { get; set; }
public int? updates_disabled { get; set; }
}
}
}
}