diff --git a/README.md b/README.md
index 340fb06..124adbc 100644
--- a/README.md
+++ b/README.md
@@ -13,22 +13,18 @@ Simple [Mastodon](https://github.com/tootsuite/mastodon) client for Linux
1. Make sure you have these dependencies:
- Package Name | Required Version
- --- |:---:
- meson | 0.50
- valac | 0.48
- libglib-2.0-dev | 2.30.0
- libjson-glib-dev | 1.4.4
- libxml2-dev | 2.9.10
- libgee-0.8-dev | 0.8.5
- libsoup2.4-dev | 2.64
- libgtk-3-dev | 3.22.0
-
- The following packages are also required, but will be installed automatically if not present in your system:
-
- Package Name | Required Version
- --- |:---:
- libhandy-1.0-dev | 1.0.0
+ Package Name | Required Version | Notes
+ --- |:---:| ---
+ meson | 0.50 |
+ valac | 0.48 |
+ libglib-2.0-dev | 2.30 |
+ libjson-glib-dev | 1.4.4 |
+ libxml2-dev | 2.9.10 |
+ libgee-0.8-dev | 0.8.5 |
+ libsoup2.4-dev | 2.64 |
+ libgtk-3-dev | 3.22 |
+ libhandy-1.0-dev | 1.0.0 | Will be attempted to install automatically if not present.
+ libsecret-1-dev | 0.20 | Optional. Used for storing accounts using Secret Service API.
2. Run `install.sh` in the project directory. The app will launch automatically on success.
diff --git a/meson.build b/meson.build
index b90bdfe..a07f92d 100644
--- a/meson.build
+++ b/meson.build
@@ -1,37 +1,11 @@
project('com.github.bleakgrey.tootle', 'vala', 'c', version: '1.0.0')
-config = configuration_data()
-config.set('EXEC_NAME', meson.project_name())
-config.set('GETTEXT_PACKAGE', meson.project_name())
-config.set('RESOURCES', '/' + '/'.join(meson.project_name().split('.')) + '/' )
-config.set('VERSION', meson.project_version())
-config.set('PREFIX', get_option('prefix'))
-config.set('NAME', 'Tootle')
-config.set('WEBSITE', 'https://github.com/bleakgrey/tootle')
-config.set('SUPPORT_WEBSITE', 'https://github.com/bleakgrey/tootle/issues')
-config.set('COPYRIGHT', '© 2018-2020 bleak_grey')
-
-gnome = import('gnome')
-i18n = import('i18n')
-
add_global_arguments([
'-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name()),
],
language: 'c',
)
-asresources = gnome.compile_resources(
- 'as-resources', 'data/gresource.xml',
- source_dir: 'data',
- c_name: 'as'
-)
-
-build_file = configure_file(
- input: 'src/Build.vala',
- output: 'Build.vala',
- configuration: config
-)
-
libhandy_dep = dependency('libhandy-1', version: '>= 1.0', required: false)
if not libhandy_dep.found()
libhandy = subproject(
@@ -51,21 +25,41 @@ if not libhandy_dep.found()
)
endif
-executable(
- meson.project_name(),
- asresources,
- build_file,
+config = configuration_data()
+config.set('EXEC_NAME', meson.project_name())
+config.set('GETTEXT_PACKAGE', meson.project_name())
+config.set('RESOURCES', '/' + '/'.join(meson.project_name().split('.')) + '/' )
+config.set('VERSION', meson.project_version())
+config.set('PREFIX', get_option('prefix'))
+config.set('NAME', 'Tootle')
+config.set('WEBSITE', 'https://github.com/bleakgrey/tootle')
+config.set('SUPPORT_WEBSITE', 'https://github.com/bleakgrey/tootle/issues')
+config.set('COPYRIGHT', '© 2018-2020 bleak_grey')
+config.set('ACCOUNT_STORE', 'FileAccountStore')
+
+gnome = import('gnome')
+i18n = import('i18n')
+
+asresources = gnome.compile_resources(
+ 'as-resources', 'data/gresource.xml',
+ source_dir: 'data',
+ c_name: 'as'
+)
+
+sources = files(
'src/Application.vala',
'src/Desktop.vala',
'src/Drawing.vala',
'src/Html.vala',
'src/Request.vala',
'src/DateTime.vala',
- 'src/InstanceAccount.vala',
+ 'src/Services/Accounts/InstanceAccount.vala',
+ 'src/Services/Accounts/AccountStore.vala',
+ 'src/Services/Accounts/FileAccountStore.vala',
+ 'src/Services/Accounts/IAccountListener.vala',
+ 'src/Services/Accounts/Mastodon/MastodonAccount.vala',
'src/Services/Streams.vala',
'src/Services/Settings.vala',
- 'src/Services/Accounts.vala',
- 'src/Services/IAccountListener.vala',
'src/Services/IStreamListener.vala',
'src/Services/Cache.vala',
'src/Services/Network.vala',
@@ -122,6 +116,27 @@ executable(
'src/Views/Hashtag.vala',
'src/Views/Lists.vala',
'src/Views/List.vala',
+)
+
+libsecret_dep = dependency('libsecret-1', required: false)
+if not libsecret_dep.found()
+ warning('Keyring support disabled. Accounts will be stored in a text file.')
+else
+ config.set('ACCOUNT_STORE','SecretAccountStore')
+ sources += 'src/Services/Accounts/'+config.get('ACCOUNT_STORE')+'.vala'
+endif
+
+build_file = configure_file(
+ input: 'src/Build.vala.in',
+ output: 'Build.vala',
+ configuration: config
+)
+
+executable(
+ meson.project_name(),
+ asresources,
+ build_file,
+ sources,
dependencies: [
dependency('gtk+-3.0', version: '>=3.22.0'),
dependency('glib-2.0', version: '>=2.30.0'),
@@ -130,6 +145,7 @@ executable(
dependency('json-glib-1.0', version: '>=1.4.4'),
dependency('libxml-2.0'),
libhandy_dep,
+ libsecret_dep,
],
install: true,
)
diff --git a/meson/post_install.py b/meson/post_install.py
old mode 100644
new mode 100755
diff --git a/src/API/Notification.vala b/src/API/Notification.vala
index 7a5cb62..392b77b 100644
--- a/src/API/Notification.vala
+++ b/src/API/Notification.vala
@@ -11,12 +11,6 @@ public class Tootle.API.Notification : Entity, Widgetizable {
}
public Soup.Message? dismiss () {
- if (kind == NotificationType.WATCHLIST) {
- if (accounts.active.cached_notifications.remove (this))
- accounts.save ();
- return null;
- }
-
if (kind == NotificationType.FOLLOW_REQUEST)
return reject_follow_request ();
diff --git a/src/API/NotificationType.vala b/src/API/NotificationType.vala
index c3f4420..4b8cee8 100644
--- a/src/API/NotificationType.vala
+++ b/src/API/NotificationType.vala
@@ -4,8 +4,7 @@ public enum Tootle.API.NotificationType {
REBLOG_REMOTE_USER, // Internal
FAVOURITE,
FOLLOW,
- FOLLOW_REQUEST, // Internal
- WATCHLIST; // Internal
+ FOLLOW_REQUEST; // Internal
public string to_string () {
switch (this) {
@@ -21,8 +20,6 @@ public enum Tootle.API.NotificationType {
return "follow";
case FOLLOW_REQUEST:
return "follow_request";
- case WATCHLIST:
- return "watchlist";
default:
warning (@"Unknown notification type: $this");
return "";
@@ -43,8 +40,6 @@ public enum Tootle.API.NotificationType {
return FOLLOW;
case "follow_request":
return FOLLOW_REQUEST;
- case "watchlist":
- return WATCHLIST;
default:
throw new Oopsie.INSTANCE (@"Unknown notification type: $str");
}
@@ -64,8 +59,6 @@ public enum Tootle.API.NotificationType {
return _("%s now follows you").printf (account.url, account.display_name);
case FOLLOW_REQUEST:
return _("%s wants to follow you").printf (account.url, account.display_name);
- case WATCHLIST:
- return _("%s posted a status").printf (account.url, account.display_name);
default:
warning (@"Unknown notification type: $this");
return "";
@@ -75,7 +68,6 @@ public enum Tootle.API.NotificationType {
public string get_icon () {
switch (this) {
case MENTION:
- case WATCHLIST:
return "user-available-symbolic";
case REBLOG:
case REBLOG_REMOTE_USER:
diff --git a/src/Application.vala b/src/Application.vala
index 3b2c9b9..2b74786 100644
--- a/src/Application.vala
+++ b/src/Application.vala
@@ -15,7 +15,7 @@ namespace Tootle {
public static Window window_dummy;
public static Settings settings;
- public static Accounts accounts;
+ public static AccountStore accounts;
public static Network network;
public static Cache cache;
public static Streams streams;
@@ -26,14 +26,13 @@ namespace Tootle {
// These are used for the GTK Inspector
public Settings app_settings { get {return Tootle.settings; } }
- public Accounts app_accounts { get {return Tootle.accounts; } }
+ public AccountStore app_accounts { get {return Tootle.accounts; } }
public Network app_network { get {return Tootle.network; } }
public Cache app_cache { get {return Tootle.cache; } }
public Streams app_streams { get {return Tootle.streams; } }
public signal void refresh ();
public signal void toast (string title);
- public signal void error (string title, string? text);
public CssProvider css_provider = new CssProvider ();
public CssProvider zoom_css_provider = new CssProvider ();
@@ -84,33 +83,36 @@ namespace Tootle {
protected override void startup () {
base.startup ();
- Build.print_info ();
- Hdy.init ();
+ try {
+ Build.print_info ();
+ Hdy.init ();
- settings = new Settings ();
- streams = new Streams ();
- accounts = new Accounts ();
- network = new Network ();
- cache = new Cache ();
- accounts.init ();
+ settings = new Settings ();
+ streams = new Streams ();
+ network = new Network ();
+ cache = new Cache ();
+ accounts = Build.get_account_store ();
+ accounts.init ();
- app.error.connect ((title, msg) => {
- inform (Gtk.MessageType.ERROR, title, msg);
- });
+ css_provider.load_from_resource (@"$(Build.RESOURCES)app.css");
+ StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
+ StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), zoom_css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
- window_dummy = new Window ();
- add_window (window_dummy);
-
- css_provider.load_from_resource (@"$(Build.RESOURCES)app.css");
- StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
- StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), zoom_css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
+ window_dummy = new Window ();
+ add_window (window_dummy);
+ }
+ catch (Error e) {
+ var msg = _("Could not start application: %s").printf (e.message);
+ inform (Gtk.MessageType.ERROR, _("Error"), msg);
+ error (msg);
+ }
set_accels_for_action ("app.about", ACCEL_ABOUT);
set_accels_for_action ("app.compose", ACCEL_NEW_POST);
set_accels_for_action ("app.back", ACCEL_BACK);
set_accels_for_action ("app.refresh", ACCEL_REFRESH);
set_accels_for_action ("app.search", ACCEL_SEARCH);
- set_accels_for_action ("app.switch-timeline(0)", ACCEL_TIMELINE_0);
+ set_accels_for_action ("app.switch-timeline(0)", ACCEL_TIMELINE_0); //TODO: There's no action for handling these
set_accels_for_action ("app.switch-timeline(1)", ACCEL_TIMELINE_1);
set_accels_for_action ("app.switch-timeline(2)", ACCEL_TIMELINE_2);
set_accels_for_action ("app.switch-timeline(3)", ACCEL_TIMELINE_3);
@@ -138,10 +140,11 @@ namespace Tootle {
}
public void present_window () {
- if (accounts.is_empty ()) {
+ if (accounts.saved.is_empty) {
message ("Presenting NewAccount dialog");
if (new_account_window == null)
new Dialogs.NewAccount ();
+ new_account_window.present ();
}
else {
message ("Presenting MainWindow");
@@ -152,7 +155,7 @@ namespace Tootle {
}
public bool on_window_closed () {
- if (!settings.work_in_background || accounts.is_empty ())
+ if (!settings.work_in_background || accounts.saved.is_empty)
app.remove_window (window_dummy);
return false;
}
diff --git a/src/Build.vala b/src/Build.vala.in
similarity index 92%
rename from src/Build.vala
rename to src/Build.vala.in
index 37690aa..0a5cdfc 100644
--- a/src/Build.vala
+++ b/src/Build.vala.in
@@ -38,4 +38,8 @@ public class Build {
return GLib.Environment.get_os_info (key) ?? "Unknown";
}
+ public static Tootle.AccountStore get_account_store () {
+ return new Tootle.@ACCOUNT_STORE@ ();
+ }
+
}
diff --git a/src/Desktop.vala b/src/Desktop.vala
index 81484ca..f7fb95a 100644
--- a/src/Desktop.vala
+++ b/src/Desktop.vala
@@ -21,7 +21,7 @@ public class Tootle.Desktop {
}
catch (Error e){
warning (@"xdg-open failed too: $(e.message)");
- app.error (_("Open this URL in your browser"), uri);
+ app.inform (Gtk.MessageType.WARNING, _("Open this URL in your browser"), uri);
return false;
}
}
diff --git a/src/Dialogs/NewAccount.vala b/src/Dialogs/NewAccount.vala
index 9d6f270..1c60d40 100644
--- a/src/Dialogs/NewAccount.vala
+++ b/src/Dialogs/NewAccount.vala
@@ -93,16 +93,15 @@ public class Tootle.Dialogs.NewAccount: Hdy.Window {
async void step () throws Error {
if (stack.visible_child == done_step) {
- if (accounts.is_empty ())
- accounts.switch_account (0);
-
app.present_window ();
destroy ();
return;
}
- if (stack.visible_child == instance_step)
+ if (stack.visible_child == instance_step) {
setup_instance ();
+ yield accounts.guess_backend (account);
+ }
if (account.client_secret == null || account.client_id == null) {
yield register_client ();
@@ -113,7 +112,7 @@ public class Tootle.Dialogs.NewAccount: Hdy.Window {
}
void setup_instance () throws Error {
- message ("Checking instance URL");
+ message ("Checking instance");
var str = instance_entry.text
.replace ("/", "")
@@ -156,7 +155,7 @@ public class Tootle.Dialogs.NewAccount: Hdy.Window {
}
async void request_token () throws Error {
- if (code_entry.text.char_count () <= 10)
+ if (code_entry.text.char_count () <= 1)
throw new Oopsie.USER (_("Please enter a valid authorization code"));
message ("Requesting access token");
@@ -175,20 +174,18 @@ public class Tootle.Dialogs.NewAccount: Hdy.Window {
if (account.access_token == null)
throw new Oopsie.INSTANCE (_("Instance failed to authorize the access token"));
- message ("Trying to get the user profile");
- var profile_req = new Request.GET ("/api/v1/accounts/verify_credentials")
- .with_account (account);
- yield profile_req.await ();
+ yield account.verify_credentials ();
- var node = network.parse_node (profile_req);
- var profile = API.Account.from (node);
- account.patch (profile);
+ account = accounts.create_account (account.to_json ());
message ("Saving account");
accounts.add (account);
hello_label.label = _("Hello, %s!").printf (account.handle);
stack.visible_child = done_step;
+
+ message ("Switching to account");
+ accounts.activate (account);
}
public void redirect (string uri) {
@@ -231,3 +228,4 @@ public class Tootle.Dialogs.NewAccount: Hdy.Window {
}
}
+
diff --git a/src/Services/Accounts.vala b/src/Services/Accounts.vala
deleted file mode 100644
index 63d66b9..0000000
--- a/src/Services/Accounts.vala
+++ /dev/null
@@ -1,141 +0,0 @@
-using Gee;
-
-public class Tootle.Accounts : GLib.Object {
-
- private string dir_path;
- private string file_path;
-
- public ArrayList saved { get; set; default = new ArrayList (); }
- public InstanceAccount? active { get; set; }
-
- construct {
- dir_path = @"$(GLib.Environment.get_user_config_dir ())/$(app.application_id)";
- file_path = @"$dir_path/accounts.json";
- }
-
- public void switch_account (int id) {
- var acc = saved.@get (id);
- message (@"Switching to $(acc.handle)...");
- new Request.GET ("/api/v1/accounts/verify_credentials")
- .with_account (acc)
- .then ((sess, mess) => {
- var node = network.parse_node (mess);
- var updated = API.Account.from (node);
- acc.patch (updated);
- message ("OK: Token is valid");
- active = acc;
- settings.current_account = id;
- })
- .on_error ((code, reason) => {
- warning ("Token invalid!");
- app.error (
- _("Network Error"),
- _("The instance has invalidated this session. Please sign in again.\n\n%s").printf (reason)
- );
- })
- .exec ();
- }
-
- public void add (InstanceAccount account) {
- message (@"Adding new account: $(account.handle)");
- saved.add (account);
- save ();
- switch_account (saved.size - 1);
- account.subscribe ();
- }
-
- public void remove (InstanceAccount account) {
- account.unsubscribe ();
- saved.remove (account);
- saved.notify_property ("size");
-
- if (saved.size < 1)
- active = null;
- else {
- var id = settings.current_account - 1;
- if (id > saved.size - 1)
- id = saved.size - 1;
- else if (id < saved.size - 1)
- id = 0;
- switch_account (id);
- }
- save ();
-
- if (is_empty ())
- new Dialogs.NewAccount ();
- }
-
- public bool is_empty () {
- return saved.size == 0;
- }
-
- public void init () {
- save (false);
- load ();
-
- if (!is_empty ())
- switch_account (settings.current_account);
- }
-
- public void save (bool overwrite = true) {
- try {
- var dir = File.new_for_path (dir_path);
- if (!dir.query_exists ())
- dir.make_directory ();
-
- var file = File.new_for_path (file_path);
- if (file.query_exists () && !overwrite)
- return;
-
- var builder = new Json.Builder ();
- builder.begin_array ();
- saved.foreach ((acc) => {
- var node = acc.to_json ();
- builder.add_value (node);
- return true;
- });
- builder.end_array ();
-
- var generator = new Json.Generator ();
- generator.set_root (builder.get_root ());
- var data = generator.to_data (null);
-
- if (file.query_exists ())
- file.@delete ();
-
- FileOutputStream stream = file.create (FileCreateFlags.PRIVATE);
- stream.write (data.data);
- message ("Saved accounts");
- }
- catch (Error e){
- warning (e.message);
- }
- }
-
- private void load () {
- try {
- uint8[] data;
- string etag;
- var file = File.new_for_path (file_path);
- file.load_contents (null, out data, out etag);
- var contents = (string) data;
-
- var parser = new Json.Parser ();
- parser.load_from_data (contents, -1);
- var array = parser.get_root ().get_array ();
-
- array.foreach_element ((_arr, _i, node) => {
- var account = InstanceAccount.from (node);
- if (account != null) {
- saved.add (account);
- account.subscribe ();
- }
- });
- message (@"Loaded $(saved.size) accounts");
- }
- catch (Error e){
- warning (e.message);
- }
- }
-
-}
diff --git a/src/Services/Accounts/AccountStore.vala b/src/Services/Accounts/AccountStore.vala
new file mode 100644
index 0000000..f1b4759
--- /dev/null
+++ b/src/Services/Accounts/AccountStore.vala
@@ -0,0 +1,146 @@
+using Gee;
+
+public abstract class Tootle.AccountStore : GLib.Object {
+
+ public ArrayList saved { get; set; default = new ArrayList (); }
+ public InstanceAccount? active { get; set; default = null; }
+
+ // TODO: Make settings.current_account a string
+ public bool ensure_active_account () {
+ var has_active = false;
+
+ if (!saved.is_empty) {
+ if (settings.current_account > saved.size || settings.current_account <= 0)
+ settings.current_account = 0;
+
+ var last_account = saved[settings.current_account];
+ if (active != last_account) {
+ activate (last_account);
+ has_active = true;
+ }
+ }
+
+ if (!has_active)
+ app.present_window ();
+
+ return has_active;
+ }
+
+ public virtual void init () throws GLib.Error {
+ Mastodon.Account.register (this);
+
+ load ();
+ ensure_active_account ();
+ }
+
+ public abstract void load () throws GLib.Error;
+ public abstract void save () throws GLib.Error;
+ public void safe_save () {
+ try {
+ save ();
+ }
+ catch (GLib.Error e) {
+ warning (e.message);
+ app.inform (Gtk.MessageType.ERROR, _("Error"), e.message);
+ }
+ }
+
+ public virtual void add (InstanceAccount account) throws GLib.Error {
+ message (@"Adding new account: $(account.handle)");
+ saved.add (account);
+ save ();
+ account.subscribe ();
+ ensure_active_account ();
+ }
+
+ public virtual void remove (InstanceAccount account) throws GLib.Error {
+ message (@"Removing account: $(account.handle)");
+ account.unsubscribe ();
+ saved.remove (account);
+ saved.notify_property ("size");
+ save ();
+
+ var id = settings.current_account - 1;
+ if (saved.size < 1)
+ active = null;
+ else {
+ if (id > saved.size - 1)
+ id = saved.size - 1;
+ else if (id < saved.size - 1)
+ id = 0;
+ }
+ settings.current_account = id;
+
+ ensure_active_account ();
+ }
+
+ public void activate (InstanceAccount account) {
+ message (@"Activating $(account.handle)...");
+ account.verify_credentials.begin ((obj, res) => {
+ try {
+ account.verify_credentials.end (res);
+ account.error = null;
+ }
+ catch (Error e) {
+ warning (@"Couldn't activate account $(account.handle):");
+ warning (e.message);
+ account.error = e;
+ }
+ });
+
+ accounts.active = account;
+ settings.current_account = accounts.saved.index_of (account);
+ }
+
+ [Signal (detailed = true)]
+ public signal InstanceAccount? create_for_backend (Json.Node node);
+
+ public InstanceAccount create_account (Json.Node node) throws GLib.Error {
+ var obj = node.get_object ();
+ var backend = obj.get_string_member ("backend");
+ var handle = obj.get_string_member ("handle");
+ var account = create_for_backend[backend] (node);
+ if (account == null)
+ throw new Oopsie.INTERNAL (@"Account $handle has unknown backend: $backend");
+
+ return account;
+ }
+
+ // This is a super overcomplicated way and I don't like this.
+ // I just want to store an array with functions that return
+ // a "string?" value and keep the first non-null one.
+ //
+ // I figured signals with GSignalAccumulator could be
+ // useful here, but Vala doesn't support that either.
+ //
+ // So here we go. Vala bad. No cookie.
+ public abstract class BackendTest : GLib.Object {
+
+ public abstract string? get_backend (Json.Object obj);
+
+ }
+
+ public Gee.ArrayList backend_tests = new Gee.ArrayList ();
+
+ public async void guess_backend (InstanceAccount account) throws GLib.Error {
+ var req = new Request.GET ("/api/v1/instance")
+ .with_account (account);
+ yield req.await ();
+
+ var root = network.parse (req);
+
+ string? backend = null;
+ backend_tests.foreach (test => {
+ backend = test.get_backend (root);
+ return true;
+ });
+
+ if (backend == null)
+ throw new Oopsie.INTERNAL ("This instance is unsupported.");
+ else {
+ account.backend = backend;
+ message (@"$(account.instance) is using $(account.backend)");
+ }
+ }
+
+}
diff --git a/src/Services/Accounts/FileAccountStore.vala b/src/Services/Accounts/FileAccountStore.vala
new file mode 100644
index 0000000..aa2d73e
--- /dev/null
+++ b/src/Services/Accounts/FileAccountStore.vala
@@ -0,0 +1,66 @@
+using Gee;
+
+public class Tootle.FileAccountStore : AccountStore {
+
+ string dir_path;
+ string file_path;
+
+ construct {
+ dir_path = @"$(GLib.Environment.get_user_config_dir ())/$(app.application_id)";
+ file_path = @"$dir_path/accounts.json";
+ }
+
+ public override void load () throws GLib.Error {
+ uint8[] data;
+ string etag;
+ var file = File.new_for_path (file_path);
+ file.load_contents (null, out data, out etag);
+ var contents = (string) data;
+
+ var parser = new Json.Parser ();
+ parser.load_from_data (contents, -1);
+ var array = parser.get_root ().get_array ();
+
+ array.foreach_element ((arr, i, node) => {
+ try {
+ var account = accounts.create_account (node);
+ saved.add (account);
+ }
+ catch (Error e) {
+ warning (@"Couldn't load account $i: $(e.message)");
+ }
+ });
+
+ message (@"Loaded $(saved.size) accounts");
+ }
+
+ public override void save () throws GLib.Error {
+ var dir = File.new_for_path (dir_path);
+ if (!dir.query_exists ())
+ dir.make_directory_with_parents ();
+
+ var file = File.new_for_path (file_path);
+
+ var builder = new Json.Builder ();
+ builder.begin_array ();
+ saved.foreach ((acc) => {
+ var node = acc.to_json ();
+ builder.add_value (node);
+ return true;
+ });
+ builder.end_array ();
+
+ var generator = new Json.Generator ();
+ generator.set_root (builder.get_root ());
+ var data = generator.to_data (null);
+
+ if (file.query_exists ())
+ file.@delete ();
+
+ FileOutputStream stream = file.create (FileCreateFlags.PRIVATE);
+ stream.write (data.data);
+
+ message (@"Saved $(saved.size) accounts");
+ }
+
+}
diff --git a/src/Services/IAccountListener.vala b/src/Services/Accounts/IAccountListener.vala
similarity index 95%
rename from src/Services/IAccountListener.vala
rename to src/Services/Accounts/IAccountListener.vala
index 8c22b8d..bab756c 100644
--- a/src/Services/IAccountListener.vala
+++ b/src/Services/Accounts/IAccountListener.vala
@@ -1,7 +1,6 @@
+[Deprecated]
public interface Tootle.IAccountListener : GLib.Object {
- //TODO: Refactor into AccountHolder
-
protected void account_listener_init () {
accounts.notify["active"].connect (_on_active_acc_update);
accounts.saved.notify["size"].connect (_on_saved_accs_update);
diff --git a/src/InstanceAccount.vala b/src/Services/Accounts/InstanceAccount.vala
similarity index 80%
rename from src/InstanceAccount.vala
rename to src/Services/Accounts/InstanceAccount.vala
index b7d748b..4bf91cf 100644
--- a/src/InstanceAccount.vala
+++ b/src/Services/Accounts/InstanceAccount.vala
@@ -3,10 +3,12 @@ using Gee;
public class Tootle.InstanceAccount : API.Account, IStreamListener {
+ public string? backend { set; get; }
public string? instance { get; set; }
public string? client_id { get; set; }
public string? client_secret { get; set; }
public string? access_token { get; set; }
+ public Error? error { get; set; }
public int64 last_seen_notification { get; set; default = 0; }
public bool has_unread_notifications { get; set; default = false; }
@@ -18,30 +20,22 @@ public class Tootle.InstanceAccount : API.Account, IStreamListener {
owned get { return @"@$username@$domain"; }
}
- public new static InstanceAccount from (Json.Node node) throws Error {
- return Entity.from_json (typeof (InstanceAccount), node) as InstanceAccount;
- }
-
- public InstanceAccount () {
+ construct {
on_notification.connect (show_notification);
}
- ~InstanceAccount () {
- unsubscribe ();
- }
public InstanceAccount.empty (string instance){
Object (id: "", instance: instance);
}
-
- public InstanceAccount.from_account (API.Account account) {
- Object (id: account.id);
- patch (account);
+ ~InstanceAccount () {
+ unsubscribe ();
}
public bool is_current () {
return accounts.active.access_token == access_token;
}
+ // TODO: This should be IStreamable
public string get_stream_url () {
return @"$instance/api/v1/streaming/?stream=user&access_token=$access_token";
}
@@ -54,6 +48,17 @@ public class Tootle.InstanceAccount : API.Account, IStreamListener {
streams.unsubscribe (stream, this);
}
+ public async void verify_credentials () throws Error {
+ var req = new Request.GET ("/api/v1/accounts/verify_credentials").with_account (this);
+ yield req.await ();
+
+ var node = network.parse_node (req);
+ var updated = API.Account.from (node);
+ patch (updated);
+
+ message (@"$handle: profile updated");
+ }
+
public async Entity resolve (string url) throws Error {
message (@"Resolving URL: \"$url\"...");
var results = yield API.SearchResults.request (url, this);
@@ -62,6 +67,7 @@ public class Tootle.InstanceAccount : API.Account, IStreamListener {
return entity;
}
+ // TODO: notification actions
void show_notification (API.Notification obj) {
var title = HtmlUtils.remove_tags (obj.kind.get_desc (obj.account));
var notification = new GLib.Notification (title);
@@ -74,11 +80,6 @@ public class Tootle.InstanceAccount : API.Account, IStreamListener {
}
app.send_notification (app.application_id + ":" + obj.id.to_string (), notification);
-
- if (obj.kind == API.NotificationType.WATCHLIST) {
- cached_notifications.add (obj);
- accounts.save ();
- }
}
}
diff --git a/src/Services/Accounts/Mastodon/MastodonAccount.vala b/src/Services/Accounts/Mastodon/MastodonAccount.vala
new file mode 100644
index 0000000..e1cb25f
--- /dev/null
+++ b/src/Services/Accounts/Mastodon/MastodonAccount.vala
@@ -0,0 +1,22 @@
+public class Tootle.Mastodon.Account : InstanceAccount {
+
+ public const string BACKEND = "Mastodon";
+
+ class Test : AccountStore.BackendTest {
+
+ public override string? get_backend (Json.Object obj) {
+ return BACKEND; // Always treat instances as compatible with Mastodon
+ }
+
+ }
+
+ public static void register (AccountStore store) {
+ store.backend_tests.add (new Test ());
+ store.create_for_backend[BACKEND].connect ((node) => {
+ var account = Entity.from_json (typeof (Account), node) as Account;
+ account.backend = BACKEND;
+ return account;
+ });
+ }
+
+}
diff --git a/src/Services/Accounts/SecretAccountStore.vala b/src/Services/Accounts/SecretAccountStore.vala
new file mode 100644
index 0000000..d683639
--- /dev/null
+++ b/src/Services/Accounts/SecretAccountStore.vala
@@ -0,0 +1,110 @@
+using Secret;
+
+public class Tootle.SecretAccountStore : AccountStore {
+
+ const string VERSION = "1";
+
+ Secret.Schema schema;
+ GLib.HashTable schema_attributes;
+
+ public override void init () throws GLib.Error {
+ message (@"Using libsecret v$(Secret.MAJOR_VERSION).$(Secret.MINOR_VERSION).$(Secret.MICRO_VERSION)");
+
+ schema_attributes = new GLib.HashTable (str_hash, str_equal);
+ schema_attributes["login"] = SchemaAttributeType.STRING;
+ schema_attributes["version"] = SchemaAttributeType.STRING;
+ schema = new Secret.Schema.newv (
+ Build.DOMAIN,
+ Secret.SchemaFlags.NONE,
+ schema_attributes
+ );
+
+ base.init ();
+ }
+
+ public override void load () throws GLib.Error {
+ var attrs = new GLib.HashTable (str_hash, str_equal);
+ attrs["version"] = VERSION;
+
+ var secrets = Secret.password_searchv_sync (
+ schema,
+ attrs,
+ Secret.SearchFlags.ALL,
+ null
+ );
+
+ secrets.foreach (item => {
+ var account = secret_to_account (item);
+ if (account != null)
+ saved.add (account);
+ });
+
+ message (@"Loaded $(saved.size) accounts");
+ }
+
+ public override void save () throws GLib.Error {
+ saved.foreach (account => {
+ account_to_secret (account);
+ return true;
+ });
+ message (@"Saved $(saved.size) accounts");
+ }
+
+ public override void remove (InstanceAccount account) throws GLib.Error {
+ base.remove (account);
+
+ var attrs = new GLib.HashTable (str_hash, str_equal);
+ attrs["version"] = VERSION;
+ attrs["login"] = account.handle;
+
+ Secret.password_clearv_sync (
+ schema,
+ attrs,
+ null
+ );
+ }
+
+ void account_to_secret (InstanceAccount account) {
+ var attrs = new GLib.HashTable (str_hash, str_equal);
+ attrs["login"] = account.handle;
+ attrs["version"] = VERSION;
+
+ var generator = new Json.Generator ();
+ generator.set_root (account.to_json ());
+ var secret = generator.to_data (null);
+ var label = _("%s Account").printf (account.backend);
+
+ try {
+ Secret.password_storev_sync (
+ schema,
+ attrs,
+ Secret.COLLECTION_DEFAULT,
+ label,
+ secret,
+ null
+ );
+ }
+ catch (GLib.Error e) {
+ warning (e.message);
+ app.inform (Gtk.MessageType.ERROR, _("Error"), e.message);
+ }
+
+ message (@"Saved secret for $(account.handle)");
+ }
+
+ InstanceAccount? secret_to_account (Secret.Retrievable item) {
+ InstanceAccount? account = null;
+ try {
+ var secret = item.retrieve_secret_sync ();
+ var contents = secret.get_text ();
+ var parser = new Json.Parser ();
+ parser.load_from_data (contents, -1);
+ account = accounts.create_account (parser.get_root ());
+ }
+ catch (GLib.Error e) {
+ warning (e.message);
+ }
+ return account;
+ }
+
+}
diff --git a/src/Services/Network.vala b/src/Services/Network.vala
index fa2839c..dee8620 100644
--- a/src/Services/Network.vala
+++ b/src/Services/Network.vala
@@ -20,7 +20,6 @@ public class Tootle.Network : GLib.Object {
session = new Soup.Session ();
session.ssl_strict = true;
session.ssl_use_system_ca_file = true;
- session.timeout = 15;
session.request_unqueued.connect (msg => {
requests_processing--;
if (requests_processing <= 0)
diff --git a/src/Services/Streams.vala b/src/Services/Streams.vala
index cf6e767..355ad34 100644
--- a/src/Services/Streams.vala
+++ b/src/Services/Streams.vala
@@ -30,7 +30,7 @@ public class Tootle.Streams : Object {
}
public bool start () {
- info (@"Opening stream: $name");
+ message (@"Opening stream: $name");
network.session.websocket_connect_async.begin (msg, null, null, null, (obj, res) => {
socket = network.session.websocket_connect_async.end (res);
socket.error.connect (on_error);
@@ -69,7 +69,7 @@ public class Tootle.Streams : Object {
GLib.Timeout.add_seconds (timeout, start);
timeout = int.min (timeout*2, 6);
}
- warning (@"Closing stream: $name");
+ message (@"Closing stream: $name");
}
void on_message (int i, Bytes bytes) {
diff --git a/src/Views/Notifications.vala b/src/Views/Notifications.vala
index 6c14eaa..c9cd1a9 100644
--- a/src/Views/Notifications.vala
+++ b/src/Views/Notifications.vala
@@ -25,7 +25,7 @@ public class Tootle.Views.Notifications : Views.Timeline, IAccountListener, IStr
needs_attention = false;
account.has_unread_notifications = false;
account.last_seen_notification = last_id;
- accounts.save ();
+ accounts.safe_save ();
}
}
@@ -39,7 +39,7 @@ public class Tootle.Views.Notifications : Views.Timeline, IAccountListener, IStr
needs_attention = has_unread () && !current;
if (needs_attention)
- accounts.save ();
+ accounts.safe_save ();
}
public override void on_account_changed (InstanceAccount? acc) {
diff --git a/src/Widgets/AccountsButton.vala b/src/Widgets/AccountsButton.vala
index 462fbca..3df784e 100644
--- a/src/Widgets/AccountsButton.vala
+++ b/src/Widgets/AccountsButton.vala
@@ -43,7 +43,13 @@ public class Tootle.Widgets.AccountsButton : Gtk.MenuButton, IAccountListener {
);
if (forget) {
button.active = false;
- accounts.remove (account);
+ try {
+ accounts.remove (account);
+ }
+ catch (Error e) {
+ warning (e.message);
+ app.inform (Gtk.MessageType.ERROR, _("Error"), e.message);
+ }
}
}
@@ -130,8 +136,9 @@ public class Tootle.Widgets.AccountsButton : Gtk.MenuButton, IAccountListener {
var account = accounts.saved.@get (i);
if (accounts.active == account)
return;
+ else
+ accounts.activate (account);
- accounts.switch_account (i);
popover.popdown ();
}
diff --git a/src/Widgets/Attachment/Slot.vala b/src/Widgets/Attachment/Slot.vala
index 3b12db5..64eea77 100644
--- a/src/Widgets/Attachment/Slot.vala
+++ b/src/Widgets/Attachment/Slot.vala
@@ -48,7 +48,7 @@ public class Tootle.Widgets.Attachment.Slot : FlowBoxChild {
Desktop.open_uri (path);
}
catch (Error e) {
- app.error (_("Error"), e.message);
+ app.inform (Gtk.MessageType.ERROR, _("Error"), e.message);
}
});
}