Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion src/Services/Network/Network.vala
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
public class Tuba.Network : GLib.Object {

public signal void started ();
public signal void finished ();

Expand Down Expand Up @@ -106,6 +105,35 @@ public class Tuba.Network : GLib.Object {
});
}

public async GLib.InputStream queue_v2 (
owned Soup.Message msg,
GLib.Cancellable? cancellable,
out Soup.MessageHeaders response_headers
) throws GLib.Error, Oopsie {
requests_processing++;

GLib.InputStream in_stream = yield session.send_async (msg, 0, cancellable);
var status = msg.status_code;
response_headers = msg.response_headers;

if (status >= 200 && status < 300)
return in_stream;

unowned string error_msg = msg.reason_phrase;
try {
var parser = yield Network.get_parser_from_inputstream_async (in_stream);
var root = network.parse (parser);
if (root != null) {
error_msg = root.has_member ("message")
? root.get_string_member_with_default ("message", msg.reason_phrase)
: root.get_string_member_with_default ("error", msg.reason_phrase);
}
} catch {}

critical (@"Request \"$(msg.uri.to_string ())\" failed: $status $(msg.reason_phrase) $error_msg");
throw new Oopsie.INSTANCE (error_msg);
}

public void on_error (int32 code, string message) {
warning (message);
app.toast (message, 0);
Expand All @@ -128,6 +156,12 @@ public class Tuba.Network : GLib.Object {
return parser;
}

public static async Json.Parser get_parser_from_inputstream_async (InputStream in_stream) throws Error {
var parser = new Json.Parser ();
yield parser.load_from_stream_async (in_stream);
return parser;
}

public static Json.Array? get_array_mstd (Json.Parser parser) {
return parser.get_root ().get_array ();
}
Expand Down
159 changes: 159 additions & 0 deletions src/Services/Network/RequestV2.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
public class Tuba.RequestV2 : GLib.Object {
public enum Method {
GET,
POST,
PUT,
DELETE,
PATCH;

public string to_string () {
switch (this) {
case GET: return "GET";
case POST: return "POST";
case PUT: return "PUT";
case DELETE: return "DELETE";
case PATCH: return "PATCH";
default: assert_not_reached ();
}
}
}

public Method method { get; private set; default = GET; }
public string url { get; private set; }
public Soup.MessagePriority priority { get; set; default = NORMAL; }
public GLib.Cancellable? cancellable { get; set; default = null; } // priv?
public InstanceAccount? account { get; set; default = null; }
public string? force_token { get; set; default = null; }
public bool no_auth { get; set; default = false; }
public bool cache { get; set; default = true; }
public weak Gtk.Widget? ctx {
set {
this._ctx = value;
if (this._ctx != null) {
this._ctx.destroy.connect (on_ctx_destroy);
}
}
}

private GLib.HashTable<string,string> parameters = new GLib.HashTable<string,string> (str_hash, str_equal);
private string? content_type { get; set; default = null; }
private weak Gtk.Widget? _ctx = null;
private Soup.Multipart? form_data = null;
private Bytes? body_bytes = null;

private void on_ctx_destroy () {
this.cancellable.cancel ();
this.ctx = null;
}

public RequestV2 (string url, Method method = GET) {
this.method = method;
this.url = url;
}

public void add_parameter (string key, string value) {
parameters.insert (
GLib.Uri.escape_string (key, "[]"),
GLib.Uri.escape_string (value)
);
}

public bool remove_parameter (string key) {
string final_key = GLib.Uri.escape_string (key, "[]");
if (!this.parameters.contains (final_key)) return false;

return parameters.foreach_remove ((p_key, p_val) => {
return p_key == final_key || p_key == @"$final_key[]";
}) > 0;
}

public void add_parameter_array (string key, string[] values) {
if (values.length == 0) return;

string final_key = key;
if (!final_key.has_suffix ("[]")) final_key = @"$key[]";
foreach (unowned string value in values) {
add_parameter (final_key, value);
}
}

public void add_form_data (string name, string val) {
if (this.form_data == null)
this.form_data = new Soup.Multipart (Soup.FORM_MIME_TYPE_MULTIPART);
this.form_data.append_form_string (name, val);
}

public void add_form_data_file (string name, string mime, Bytes buffer) {
if (this.form_data == null)
this.form_data = new Soup.Multipart (Soup.FORM_MIME_TYPE_MULTIPART);
this.form_data.append_form_file (name, mime.replace ("/", "."), mime, buffer);
}

public void set_body (string? content_type, Bytes? bytes) {
this.content_type = content_type;
body_bytes = bytes;
}

public void set_body_from_json (Json.Builder json_builder) {
Json.Generator generator = new Json.Generator ();
generator.set_root (json_builder.get_root ());
set_body ("application/json", new Bytes.take (generator.to_data (null).data));
}

public async GLib.InputStream exec (out Soup.MessageHeaders response_headers) throws GLib.Error, Oopsie {
if (this.cancellable != null) this.cancellable.cancel ();
this.cancellable = new GLib.Cancellable ();

string final_url = build_final_url ();
Soup.Message message;
if (this.form_data == null) {
GLib.Uri final_uri = GLib.Uri.parse (final_url, UriFlags.ENCODED_PATH | UriFlags.ENCODED_QUERY);
message = new Soup.Message.from_uri (this.method.to_string (), final_uri);
} else {
message = new Soup.Message.from_multipart (final_url, this.form_data);
// POST is default for multipart
if (this.method != POST) message.method = this.method.to_string ();
}

if (!no_auth) {
if (force_token != null) {
message.request_headers.append ("Authorization", @"Bearer $force_token");
} else if (account != null && account.access_token != null) {
message.request_headers.append ("Authorization", @"Bearer $(account.access_token)");
}
} else {
message.request_headers.remove ("Authorization");
}

if (!cache) message.disable_feature (typeof (Soup.Cache));
message.priority = priority;

if (this.content_type != null && this.body_bytes != null)
message.set_request_body_from_bytes (this.content_type, this.body_bytes);

return yield network.queue_v2 (message, this.cancellable, out response_headers);
// TODO: ensure body_bytes = ctx = null...
}

private string build_final_url () {
string final_url = this.account != null && this.url.has_prefix ("/")
? @"$(this.account.instance)$(this.url)"
: this.url;
final_url += @"$("?" in this.url ? "&" : "?")$(parameters_to_string ())";
return final_url;
}

private string parameters_to_string () {
string res = "";
if (this.parameters.length == 0) return res;

int i = 0;
this.parameters.foreach ((key, val) => {
i++;
res += @"$key=$val"; // already escaped
if (i < this.parameters.length) res += "&";
});

return (owned) res;
}
}
1 change: 1 addition & 0 deletions src/Services/Network/meson.build
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
sources += files(
'Network.vala',
'Request.vala',
'RequestV2.vala',
'Streamable.vala',
'Streams.vala',
)
59 changes: 36 additions & 23 deletions src/Views/Timeline.vala
Original file line number Diff line number Diff line change
Expand Up @@ -212,33 +212,46 @@ public class Tuba.Views.Timeline : AccountHolder, Streamable, Views.ContentBase
}

public virtual bool request () {
append_params (new Request.GET (get_req_url ()))
.with_account (account)
.with_ctx (this)
.with_extra_data (Tuba.Network.ExtraData.RESPONSE_HEADERS)
.then ((in_stream, headers) => {
var parser = Network.get_parser_from_inputstream (in_stream);

Object[] to_add = {};
Network.parse_array (parser, node => {
var e = Tuba.Helper.Entity.from_json (node, accepts);
if (!(should_hide (e))) to_add += e;
});
model.splice (model.get_n_items (), 0, to_add);

if (headers != null)
get_pages (headers.get_one ("Link"));

if (to_add.length == 0)
on_content_changed ();
on_request_finish ();
})
.on_error (on_error)
.exec ();
var req = new RequestV2 (get_req_url ()) {
account = account,
ctx = this
};

if (page_next == null)
req.add_parameter ("limit", settings.timeline_page_size.clamp (this.batch_size_min, 40).to_string ());

request_async.begin (req);
return GLib.Source.REMOVE;
}

private async void request_async (RequestV2 req) {
GLib.InputStream in_stream;
Soup.MessageHeaders response_headers;

try {
in_stream = yield req.exec (out response_headers);
Json.Parser parser = yield Network.get_parser_from_inputstream_async (in_stream);

Object[] to_add = {};
Network.parse_array (parser, node => {
var e = Helper.Entity.from_json (node, accepts);
if (!(should_hide (e))) to_add += e;
});
model.splice (model.get_n_items (), 0, to_add);

if (response_headers != null)
get_pages (response_headers.get_one ("Link"));

if (to_add.length == 0)
on_content_changed ();
on_request_finish ();
} catch (GLib.IOError.CANCELLED e) {
debug ("Message is cancelled.");
} catch (GLib.Error e) {
on_error (e.code, e.message);
}
}

public override void on_error (int32 code, string reason) {
if (base_status == null) {
warning (@"Error while refreshing $label: $code $reason");
Expand Down
Loading