Refactor accounts (#284)

This commit is contained in:
Bleak Grey 2021-02-12 18:26:37 +03:00 committed by GitHub
parent 0e65502349
commit 7d85bc5660
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 481 additions and 269 deletions

View File

@ -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.

View File

@ -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,
)

0
meson/post_install.py Normal file → Executable file
View File

View File

@ -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 ();

View File

@ -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 _("<span underline=\"none\"><a href=\"%s\">%s</a> now follows you</span>").printf (account.url, account.display_name);
case FOLLOW_REQUEST:
return _("<span underline=\"none\"><a href=\"%s\">%s</a> wants to follow you</span>").printf (account.url, account.display_name);
case WATCHLIST:
return _("<span underline=\"none\"><a href=\"%s\">%s</a> posted a status</span>").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:

View File

@ -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;
}

View File

@ -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@ ();
}
}

View File

@ -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;
}
}

View File

@ -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 {
}
}

View File

@ -1,141 +0,0 @@
using Gee;
public class Tootle.Accounts : GLib.Object {
private string dir_path;
private string file_path;
public ArrayList<InstanceAccount> saved { get; set; default = new ArrayList<InstanceAccount> (); }
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);
}
}
}

View File

@ -0,0 +1,146 @@
using Gee;
public abstract class Tootle.AccountStore : GLib.Object {
public ArrayList<InstanceAccount> saved { get; set; default = new ArrayList<InstanceAccount> (); }
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<BackendTest> backend_tests = new Gee.ArrayList<BackendTest> ();
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)");
}
}
}

View File

@ -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");
}
}

View File

@ -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);

View File

@ -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 ();
}
}
}

View File

@ -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;
});
}
}

View File

@ -0,0 +1,110 @@
using Secret;
public class Tootle.SecretAccountStore : AccountStore {
const string VERSION = "1";
Secret.Schema schema;
GLib.HashTable<string,SchemaAttributeType> 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<string,SchemaAttributeType> (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<string,string> (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<string,string> (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<string,string> (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;
}
}

View File

@ -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)

View File

@ -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) {

View File

@ -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) {

View File

@ -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 ();
}

View File

@ -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);
}
});
}