Refactor accounts (#284)
This commit is contained in:
parent
0e65502349
commit
7d85bc5660
28
README.md
28
README.md
|
@ -13,22 +13,18 @@ Simple [Mastodon](https://github.com/tootsuite/mastodon) client for Linux
|
||||||
|
|
||||||
1. Make sure you have these dependencies:
|
1. Make sure you have these dependencies:
|
||||||
|
|
||||||
Package Name | Required Version
|
Package Name | Required Version | Notes
|
||||||
--- |:---:
|
--- |:---:| ---
|
||||||
meson | 0.50
|
meson | 0.50 |
|
||||||
valac | 0.48
|
valac | 0.48 |
|
||||||
libglib-2.0-dev | 2.30.0
|
libglib-2.0-dev | 2.30 |
|
||||||
libjson-glib-dev | 1.4.4
|
libjson-glib-dev | 1.4.4 |
|
||||||
libxml2-dev | 2.9.10
|
libxml2-dev | 2.9.10 |
|
||||||
libgee-0.8-dev | 0.8.5
|
libgee-0.8-dev | 0.8.5 |
|
||||||
libsoup2.4-dev | 2.64
|
libsoup2.4-dev | 2.64 |
|
||||||
libgtk-3-dev | 3.22.0
|
libgtk-3-dev | 3.22 |
|
||||||
|
libhandy-1.0-dev | 1.0.0 | Will be attempted to install automatically if not present.
|
||||||
The following packages are also required, but will be installed automatically if not present in your system:
|
libsecret-1-dev | 0.20 | Optional. Used for storing accounts using Secret Service API.
|
||||||
|
|
||||||
Package Name | Required Version
|
|
||||||
--- |:---:
|
|
||||||
libhandy-1.0-dev | 1.0.0
|
|
||||||
|
|
||||||
|
|
||||||
2. Run `install.sh` in the project directory. The app will launch automatically on success.
|
2. Run `install.sh` in the project directory. The app will launch automatically on success.
|
||||||
|
|
82
meson.build
82
meson.build
|
@ -1,37 +1,11 @@
|
||||||
project('com.github.bleakgrey.tootle', 'vala', 'c', version: '1.0.0')
|
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([
|
add_global_arguments([
|
||||||
'-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name()),
|
'-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name()),
|
||||||
],
|
],
|
||||||
language: 'c',
|
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)
|
libhandy_dep = dependency('libhandy-1', version: '>= 1.0', required: false)
|
||||||
if not libhandy_dep.found()
|
if not libhandy_dep.found()
|
||||||
libhandy = subproject(
|
libhandy = subproject(
|
||||||
|
@ -51,21 +25,41 @@ if not libhandy_dep.found()
|
||||||
)
|
)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
executable(
|
config = configuration_data()
|
||||||
meson.project_name(),
|
config.set('EXEC_NAME', meson.project_name())
|
||||||
asresources,
|
config.set('GETTEXT_PACKAGE', meson.project_name())
|
||||||
build_file,
|
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/Application.vala',
|
||||||
'src/Desktop.vala',
|
'src/Desktop.vala',
|
||||||
'src/Drawing.vala',
|
'src/Drawing.vala',
|
||||||
'src/Html.vala',
|
'src/Html.vala',
|
||||||
'src/Request.vala',
|
'src/Request.vala',
|
||||||
'src/DateTime.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/Streams.vala',
|
||||||
'src/Services/Settings.vala',
|
'src/Services/Settings.vala',
|
||||||
'src/Services/Accounts.vala',
|
|
||||||
'src/Services/IAccountListener.vala',
|
|
||||||
'src/Services/IStreamListener.vala',
|
'src/Services/IStreamListener.vala',
|
||||||
'src/Services/Cache.vala',
|
'src/Services/Cache.vala',
|
||||||
'src/Services/Network.vala',
|
'src/Services/Network.vala',
|
||||||
|
@ -122,6 +116,27 @@ executable(
|
||||||
'src/Views/Hashtag.vala',
|
'src/Views/Hashtag.vala',
|
||||||
'src/Views/Lists.vala',
|
'src/Views/Lists.vala',
|
||||||
'src/Views/List.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: [
|
dependencies: [
|
||||||
dependency('gtk+-3.0', version: '>=3.22.0'),
|
dependency('gtk+-3.0', version: '>=3.22.0'),
|
||||||
dependency('glib-2.0', version: '>=2.30.0'),
|
dependency('glib-2.0', version: '>=2.30.0'),
|
||||||
|
@ -130,6 +145,7 @@ executable(
|
||||||
dependency('json-glib-1.0', version: '>=1.4.4'),
|
dependency('json-glib-1.0', version: '>=1.4.4'),
|
||||||
dependency('libxml-2.0'),
|
dependency('libxml-2.0'),
|
||||||
libhandy_dep,
|
libhandy_dep,
|
||||||
|
libsecret_dep,
|
||||||
],
|
],
|
||||||
install: true,
|
install: true,
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,12 +11,6 @@ public class Tootle.API.Notification : Entity, Widgetizable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Soup.Message? dismiss () {
|
public Soup.Message? dismiss () {
|
||||||
if (kind == NotificationType.WATCHLIST) {
|
|
||||||
if (accounts.active.cached_notifications.remove (this))
|
|
||||||
accounts.save ();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (kind == NotificationType.FOLLOW_REQUEST)
|
if (kind == NotificationType.FOLLOW_REQUEST)
|
||||||
return reject_follow_request ();
|
return reject_follow_request ();
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,7 @@ public enum Tootle.API.NotificationType {
|
||||||
REBLOG_REMOTE_USER, // Internal
|
REBLOG_REMOTE_USER, // Internal
|
||||||
FAVOURITE,
|
FAVOURITE,
|
||||||
FOLLOW,
|
FOLLOW,
|
||||||
FOLLOW_REQUEST, // Internal
|
FOLLOW_REQUEST; // Internal
|
||||||
WATCHLIST; // Internal
|
|
||||||
|
|
||||||
public string to_string () {
|
public string to_string () {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
|
@ -21,8 +20,6 @@ public enum Tootle.API.NotificationType {
|
||||||
return "follow";
|
return "follow";
|
||||||
case FOLLOW_REQUEST:
|
case FOLLOW_REQUEST:
|
||||||
return "follow_request";
|
return "follow_request";
|
||||||
case WATCHLIST:
|
|
||||||
return "watchlist";
|
|
||||||
default:
|
default:
|
||||||
warning (@"Unknown notification type: $this");
|
warning (@"Unknown notification type: $this");
|
||||||
return "";
|
return "";
|
||||||
|
@ -43,8 +40,6 @@ public enum Tootle.API.NotificationType {
|
||||||
return FOLLOW;
|
return FOLLOW;
|
||||||
case "follow_request":
|
case "follow_request":
|
||||||
return FOLLOW_REQUEST;
|
return FOLLOW_REQUEST;
|
||||||
case "watchlist":
|
|
||||||
return WATCHLIST;
|
|
||||||
default:
|
default:
|
||||||
throw new Oopsie.INSTANCE (@"Unknown notification type: $str");
|
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);
|
return _("<span underline=\"none\"><a href=\"%s\">%s</a> now follows you</span>").printf (account.url, account.display_name);
|
||||||
case FOLLOW_REQUEST:
|
case FOLLOW_REQUEST:
|
||||||
return _("<span underline=\"none\"><a href=\"%s\">%s</a> wants to follow you</span>").printf (account.url, account.display_name);
|
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:
|
default:
|
||||||
warning (@"Unknown notification type: $this");
|
warning (@"Unknown notification type: $this");
|
||||||
return "";
|
return "";
|
||||||
|
@ -75,7 +68,6 @@ public enum Tootle.API.NotificationType {
|
||||||
public string get_icon () {
|
public string get_icon () {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case MENTION:
|
case MENTION:
|
||||||
case WATCHLIST:
|
|
||||||
return "user-available-symbolic";
|
return "user-available-symbolic";
|
||||||
case REBLOG:
|
case REBLOG:
|
||||||
case REBLOG_REMOTE_USER:
|
case REBLOG_REMOTE_USER:
|
||||||
|
|
|
@ -15,7 +15,7 @@ namespace Tootle {
|
||||||
public static Window window_dummy;
|
public static Window window_dummy;
|
||||||
|
|
||||||
public static Settings settings;
|
public static Settings settings;
|
||||||
public static Accounts accounts;
|
public static AccountStore accounts;
|
||||||
public static Network network;
|
public static Network network;
|
||||||
public static Cache cache;
|
public static Cache cache;
|
||||||
public static Streams streams;
|
public static Streams streams;
|
||||||
|
@ -26,14 +26,13 @@ namespace Tootle {
|
||||||
|
|
||||||
// These are used for the GTK Inspector
|
// These are used for the GTK Inspector
|
||||||
public Settings app_settings { get {return Tootle.settings; } }
|
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 Network app_network { get {return Tootle.network; } }
|
||||||
public Cache app_cache { get {return Tootle.cache; } }
|
public Cache app_cache { get {return Tootle.cache; } }
|
||||||
public Streams app_streams { get {return Tootle.streams; } }
|
public Streams app_streams { get {return Tootle.streams; } }
|
||||||
|
|
||||||
public signal void refresh ();
|
public signal void refresh ();
|
||||||
public signal void toast (string title);
|
public signal void toast (string title);
|
||||||
public signal void error (string title, string? text);
|
|
||||||
|
|
||||||
public CssProvider css_provider = new CssProvider ();
|
public CssProvider css_provider = new CssProvider ();
|
||||||
public CssProvider zoom_css_provider = new CssProvider ();
|
public CssProvider zoom_css_provider = new CssProvider ();
|
||||||
|
@ -84,33 +83,36 @@ namespace Tootle {
|
||||||
|
|
||||||
protected override void startup () {
|
protected override void startup () {
|
||||||
base.startup ();
|
base.startup ();
|
||||||
Build.print_info ();
|
try {
|
||||||
Hdy.init ();
|
Build.print_info ();
|
||||||
|
Hdy.init ();
|
||||||
|
|
||||||
settings = new Settings ();
|
settings = new Settings ();
|
||||||
streams = new Streams ();
|
streams = new Streams ();
|
||||||
accounts = new Accounts ();
|
network = new Network ();
|
||||||
network = new Network ();
|
cache = new Cache ();
|
||||||
cache = new Cache ();
|
accounts = Build.get_account_store ();
|
||||||
accounts.init ();
|
accounts.init ();
|
||||||
|
|
||||||
app.error.connect ((title, msg) => {
|
css_provider.load_from_resource (@"$(Build.RESOURCES)app.css");
|
||||||
inform (Gtk.MessageType.ERROR, title, msg);
|
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 ();
|
window_dummy = new Window ();
|
||||||
add_window (window_dummy);
|
add_window (window_dummy);
|
||||||
|
}
|
||||||
css_provider.load_from_resource (@"$(Build.RESOURCES)app.css");
|
catch (Error e) {
|
||||||
StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
|
var msg = _("Could not start application: %s").printf (e.message);
|
||||||
StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), zoom_css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
|
inform (Gtk.MessageType.ERROR, _("Error"), msg);
|
||||||
|
error (msg);
|
||||||
|
}
|
||||||
|
|
||||||
set_accels_for_action ("app.about", ACCEL_ABOUT);
|
set_accels_for_action ("app.about", ACCEL_ABOUT);
|
||||||
set_accels_for_action ("app.compose", ACCEL_NEW_POST);
|
set_accels_for_action ("app.compose", ACCEL_NEW_POST);
|
||||||
set_accels_for_action ("app.back", ACCEL_BACK);
|
set_accels_for_action ("app.back", ACCEL_BACK);
|
||||||
set_accels_for_action ("app.refresh", ACCEL_REFRESH);
|
set_accels_for_action ("app.refresh", ACCEL_REFRESH);
|
||||||
set_accels_for_action ("app.search", ACCEL_SEARCH);
|
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(1)", ACCEL_TIMELINE_1);
|
||||||
set_accels_for_action ("app.switch-timeline(2)", ACCEL_TIMELINE_2);
|
set_accels_for_action ("app.switch-timeline(2)", ACCEL_TIMELINE_2);
|
||||||
set_accels_for_action ("app.switch-timeline(3)", ACCEL_TIMELINE_3);
|
set_accels_for_action ("app.switch-timeline(3)", ACCEL_TIMELINE_3);
|
||||||
|
@ -138,10 +140,11 @@ namespace Tootle {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void present_window () {
|
public void present_window () {
|
||||||
if (accounts.is_empty ()) {
|
if (accounts.saved.is_empty) {
|
||||||
message ("Presenting NewAccount dialog");
|
message ("Presenting NewAccount dialog");
|
||||||
if (new_account_window == null)
|
if (new_account_window == null)
|
||||||
new Dialogs.NewAccount ();
|
new Dialogs.NewAccount ();
|
||||||
|
new_account_window.present ();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
message ("Presenting MainWindow");
|
message ("Presenting MainWindow");
|
||||||
|
@ -152,7 +155,7 @@ namespace Tootle {
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool on_window_closed () {
|
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);
|
app.remove_window (window_dummy);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,4 +38,8 @@ public class Build {
|
||||||
return GLib.Environment.get_os_info (key) ?? "Unknown";
|
return GLib.Environment.get_os_info (key) ?? "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Tootle.AccountStore get_account_store () {
|
||||||
|
return new Tootle.@ACCOUNT_STORE@ ();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -21,7 +21,7 @@ public class Tootle.Desktop {
|
||||||
}
|
}
|
||||||
catch (Error e){
|
catch (Error e){
|
||||||
warning (@"xdg-open failed too: $(e.message)");
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,16 +93,15 @@ public class Tootle.Dialogs.NewAccount: Hdy.Window {
|
||||||
|
|
||||||
async void step () throws Error {
|
async void step () throws Error {
|
||||||
if (stack.visible_child == done_step) {
|
if (stack.visible_child == done_step) {
|
||||||
if (accounts.is_empty ())
|
|
||||||
accounts.switch_account (0);
|
|
||||||
|
|
||||||
app.present_window ();
|
app.present_window ();
|
||||||
destroy ();
|
destroy ();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stack.visible_child == instance_step)
|
if (stack.visible_child == instance_step) {
|
||||||
setup_instance ();
|
setup_instance ();
|
||||||
|
yield accounts.guess_backend (account);
|
||||||
|
}
|
||||||
|
|
||||||
if (account.client_secret == null || account.client_id == null) {
|
if (account.client_secret == null || account.client_id == null) {
|
||||||
yield register_client ();
|
yield register_client ();
|
||||||
|
@ -113,7 +112,7 @@ public class Tootle.Dialogs.NewAccount: Hdy.Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup_instance () throws Error {
|
void setup_instance () throws Error {
|
||||||
message ("Checking instance URL");
|
message ("Checking instance");
|
||||||
|
|
||||||
var str = instance_entry.text
|
var str = instance_entry.text
|
||||||
.replace ("/", "")
|
.replace ("/", "")
|
||||||
|
@ -156,7 +155,7 @@ public class Tootle.Dialogs.NewAccount: Hdy.Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
async void request_token () throws Error {
|
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"));
|
throw new Oopsie.USER (_("Please enter a valid authorization code"));
|
||||||
|
|
||||||
message ("Requesting access token");
|
message ("Requesting access token");
|
||||||
|
@ -175,20 +174,18 @@ public class Tootle.Dialogs.NewAccount: Hdy.Window {
|
||||||
if (account.access_token == null)
|
if (account.access_token == null)
|
||||||
throw new Oopsie.INSTANCE (_("Instance failed to authorize the access token"));
|
throw new Oopsie.INSTANCE (_("Instance failed to authorize the access token"));
|
||||||
|
|
||||||
message ("Trying to get the user profile");
|
yield account.verify_credentials ();
|
||||||
var profile_req = new Request.GET ("/api/v1/accounts/verify_credentials")
|
|
||||||
.with_account (account);
|
|
||||||
yield profile_req.await ();
|
|
||||||
|
|
||||||
var node = network.parse_node (profile_req);
|
account = accounts.create_account (account.to_json ());
|
||||||
var profile = API.Account.from (node);
|
|
||||||
account.patch (profile);
|
|
||||||
|
|
||||||
message ("Saving account");
|
message ("Saving account");
|
||||||
accounts.add (account);
|
accounts.add (account);
|
||||||
|
|
||||||
hello_label.label = _("Hello, %s!").printf (account.handle);
|
hello_label.label = _("Hello, %s!").printf (account.handle);
|
||||||
stack.visible_child = done_step;
|
stack.visible_child = done_step;
|
||||||
|
|
||||||
|
message ("Switching to account");
|
||||||
|
accounts.activate (account);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void redirect (string uri) {
|
public void redirect (string uri) {
|
||||||
|
@ -231,3 +228,4 @@ public class Tootle.Dialogs.NewAccount: Hdy.Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
|
[Deprecated]
|
||||||
public interface Tootle.IAccountListener : GLib.Object {
|
public interface Tootle.IAccountListener : GLib.Object {
|
||||||
|
|
||||||
//TODO: Refactor into AccountHolder
|
|
||||||
|
|
||||||
protected void account_listener_init () {
|
protected void account_listener_init () {
|
||||||
accounts.notify["active"].connect (_on_active_acc_update);
|
accounts.notify["active"].connect (_on_active_acc_update);
|
||||||
accounts.saved.notify["size"].connect (_on_saved_accs_update);
|
accounts.saved.notify["size"].connect (_on_saved_accs_update);
|
|
@ -3,10 +3,12 @@ using Gee;
|
||||||
|
|
||||||
public class Tootle.InstanceAccount : API.Account, IStreamListener {
|
public class Tootle.InstanceAccount : API.Account, IStreamListener {
|
||||||
|
|
||||||
|
public string? backend { set; get; }
|
||||||
public string? instance { get; set; }
|
public string? instance { get; set; }
|
||||||
public string? client_id { get; set; }
|
public string? client_id { get; set; }
|
||||||
public string? client_secret { get; set; }
|
public string? client_secret { get; set; }
|
||||||
public string? access_token { get; set; }
|
public string? access_token { get; set; }
|
||||||
|
public Error? error { get; set; }
|
||||||
|
|
||||||
public int64 last_seen_notification { get; set; default = 0; }
|
public int64 last_seen_notification { get; set; default = 0; }
|
||||||
public bool has_unread_notifications { get; set; default = false; }
|
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"; }
|
owned get { return @"@$username@$domain"; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public new static InstanceAccount from (Json.Node node) throws Error {
|
construct {
|
||||||
return Entity.from_json (typeof (InstanceAccount), node) as InstanceAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public InstanceAccount () {
|
|
||||||
on_notification.connect (show_notification);
|
on_notification.connect (show_notification);
|
||||||
}
|
}
|
||||||
~InstanceAccount () {
|
|
||||||
unsubscribe ();
|
|
||||||
}
|
|
||||||
|
|
||||||
public InstanceAccount.empty (string instance){
|
public InstanceAccount.empty (string instance){
|
||||||
Object (id: "", instance: instance);
|
Object (id: "", instance: instance);
|
||||||
}
|
}
|
||||||
|
~InstanceAccount () {
|
||||||
public InstanceAccount.from_account (API.Account account) {
|
unsubscribe ();
|
||||||
Object (id: account.id);
|
|
||||||
patch (account);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool is_current () {
|
public bool is_current () {
|
||||||
return accounts.active.access_token == access_token;
|
return accounts.active.access_token == access_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: This should be IStreamable
|
||||||
public string get_stream_url () {
|
public string get_stream_url () {
|
||||||
return @"$instance/api/v1/streaming/?stream=user&access_token=$access_token";
|
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);
|
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 {
|
public async Entity resolve (string url) throws Error {
|
||||||
message (@"Resolving URL: \"$url\"...");
|
message (@"Resolving URL: \"$url\"...");
|
||||||
var results = yield API.SearchResults.request (url, this);
|
var results = yield API.SearchResults.request (url, this);
|
||||||
|
@ -62,6 +67,7 @@ public class Tootle.InstanceAccount : API.Account, IStreamListener {
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: notification actions
|
||||||
void show_notification (API.Notification obj) {
|
void show_notification (API.Notification obj) {
|
||||||
var title = HtmlUtils.remove_tags (obj.kind.get_desc (obj.account));
|
var title = HtmlUtils.remove_tags (obj.kind.get_desc (obj.account));
|
||||||
var notification = new GLib.Notification (title);
|
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);
|
app.send_notification (app.application_id + ":" + obj.id.to_string (), notification);
|
||||||
|
|
||||||
if (obj.kind == API.NotificationType.WATCHLIST) {
|
|
||||||
cached_notifications.add (obj);
|
|
||||||
accounts.save ();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -20,7 +20,6 @@ public class Tootle.Network : GLib.Object {
|
||||||
session = new Soup.Session ();
|
session = new Soup.Session ();
|
||||||
session.ssl_strict = true;
|
session.ssl_strict = true;
|
||||||
session.ssl_use_system_ca_file = true;
|
session.ssl_use_system_ca_file = true;
|
||||||
session.timeout = 15;
|
|
||||||
session.request_unqueued.connect (msg => {
|
session.request_unqueued.connect (msg => {
|
||||||
requests_processing--;
|
requests_processing--;
|
||||||
if (requests_processing <= 0)
|
if (requests_processing <= 0)
|
||||||
|
|
|
@ -30,7 +30,7 @@ public class Tootle.Streams : Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool start () {
|
public bool start () {
|
||||||
info (@"Opening stream: $name");
|
message (@"Opening stream: $name");
|
||||||
network.session.websocket_connect_async.begin (msg, null, null, null, (obj, res) => {
|
network.session.websocket_connect_async.begin (msg, null, null, null, (obj, res) => {
|
||||||
socket = network.session.websocket_connect_async.end (res);
|
socket = network.session.websocket_connect_async.end (res);
|
||||||
socket.error.connect (on_error);
|
socket.error.connect (on_error);
|
||||||
|
@ -69,7 +69,7 @@ public class Tootle.Streams : Object {
|
||||||
GLib.Timeout.add_seconds (timeout, start);
|
GLib.Timeout.add_seconds (timeout, start);
|
||||||
timeout = int.min (timeout*2, 6);
|
timeout = int.min (timeout*2, 6);
|
||||||
}
|
}
|
||||||
warning (@"Closing stream: $name");
|
message (@"Closing stream: $name");
|
||||||
}
|
}
|
||||||
|
|
||||||
void on_message (int i, Bytes bytes) {
|
void on_message (int i, Bytes bytes) {
|
||||||
|
|
|
@ -25,7 +25,7 @@ public class Tootle.Views.Notifications : Views.Timeline, IAccountListener, IStr
|
||||||
needs_attention = false;
|
needs_attention = false;
|
||||||
account.has_unread_notifications = false;
|
account.has_unread_notifications = false;
|
||||||
account.last_seen_notification = last_id;
|
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;
|
needs_attention = has_unread () && !current;
|
||||||
if (needs_attention)
|
if (needs_attention)
|
||||||
accounts.save ();
|
accounts.safe_save ();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void on_account_changed (InstanceAccount? acc) {
|
public override void on_account_changed (InstanceAccount? acc) {
|
||||||
|
|
|
@ -43,7 +43,13 @@ public class Tootle.Widgets.AccountsButton : Gtk.MenuButton, IAccountListener {
|
||||||
);
|
);
|
||||||
if (forget) {
|
if (forget) {
|
||||||
button.active = false;
|
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);
|
var account = accounts.saved.@get (i);
|
||||||
if (accounts.active == account)
|
if (accounts.active == account)
|
||||||
return;
|
return;
|
||||||
|
else
|
||||||
|
accounts.activate (account);
|
||||||
|
|
||||||
accounts.switch_account (i);
|
|
||||||
popover.popdown ();
|
popover.popdown ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ public class Tootle.Widgets.Attachment.Slot : FlowBoxChild {
|
||||||
Desktop.open_uri (path);
|
Desktop.open_uri (path);
|
||||||
}
|
}
|
||||||
catch (Error e) {
|
catch (Error e) {
|
||||||
app.error (_("Error"), e.message);
|
app.inform (Gtk.MessageType.ERROR, _("Error"), e.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue