Commit ae4e2f22 authored by Thomas's avatar Thomas
Browse files

Add Misskey support

- Authentication via MiAuth flow
- Timelines: home, local, global, hashtag, list
- Post/create notes with media, polls, CW, visibility
- Scheduled posts (create, list, cancel)
- Delete & redraft
- Boost (renote), favourite (reaction), bookmark (favorite)
- Notifications with accept/reject follow requests
- User profiles with follow/unfollow, mute (timed), block
- Followers/following lists, muted/blocked lists
- Context/thread view (conversation + replies)
- Search: notes, accounts, hashtags
- Pin/unpin notes
- Lists: CRUD, member management, timeline
- Report abuse
- Edit profile (display name, bio, avatar, header)
- Cross-posting and cross-actions
- Custom emoji support with instance cache fallback
- Unified emoji picker (custom + standard)
- Hide unsupported features (filters, trends, suggestions, directory, announcements, domain blocking, followed tags)
parent 9f27f2ab
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -118,7 +118,8 @@ dependencies {
    implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
    implementation 'androidx.preference:preference:1.2.1'
    implementation "org.conscrypt:conscrypt-android:2.5.2"
    implementation 'com.vanniktech:emoji-one:0.6.0'
    implementation 'com.vanniktech:emoji-google:0.21.0'
    implementation 'androidx.emoji2:emoji2-emojipicker:1.5.0'
    implementation 'com.github.GrenderG:Toasty:1.5.2'
    implementation "com.github.bumptech.glide:glide:4.14.2"
    implementation "com.github.bumptech.glide:okhttp3-integration:4.14.2"
+3 −0
Original line number Diff line number Diff line
@@ -94,6 +94,9 @@
                <data
                    android:host="backtofedilab"
                    android:scheme="fedilab" />
                <data
                    android:host="misskey-auth"
                    android:scheme="fedilab" />
            </intent-filter>
        </activity>

+34 −2
Original line number Diff line number Diff line
@@ -1189,6 +1189,9 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
                    currentToken = sharedpreferences.getString(PREF_USER_TOKEN, null);
                }
                Helper.setCurrentAccount(new Account(BaseMainActivity.this).getConnectedAccount());
                if (Helper.getCurrentAccount(BaseMainActivity.this) != null) {
                    api = Helper.getCurrentAccount(BaseMainActivity.this).api;
                }
            } catch (DBException e) {
                e.printStackTrace();
            }
@@ -1269,6 +1272,16 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
                if (Helper.getCurrentAccount(BaseMainActivity.this).admin) {
                    binding.navView.getMenu().findItem(R.id.nav_administration).setVisible(true);
                }
                if (api == Account.API.MISSKEY) {
                    binding.navView.getMenu().findItem(R.id.nav_filter).setVisible(false);
                    binding.navView.getMenu().findItem(R.id.nav_followed_tags).setVisible(false);
                    binding.navView.getMenu().findItem(R.id.nav_announcements).setVisible(false);
                    binding.navView.getMenu().findItem(R.id.nav_trends).setVisible(false);
                    binding.navView.getMenu().findItem(R.id.nav_suggestions).setVisible(false);
                    binding.navView.getMenu().findItem(R.id.nav_directory).setVisible(false);
                    binding.navView.getMenu().findItem(R.id.nav_about_instance).setVisible(false);
                    binding.navView.getMenu().findItem(R.id.nav_instance_info).setVisible(false);
                }
                if (bottomMenu != null) {
                    //ManageClick on bottom menu items
                    if (binding.bottomNavView.findViewById(R.id.nav_home) != null) {
@@ -1636,8 +1649,27 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
        if (emojis == null || !emojis.containsKey(BaseMainActivity.currentInstance) || emojis.get(BaseMainActivity.currentInstance) == null) {
            new Thread(() -> {
                try {
                    if (api == app.fedilab.android.mastodon.client.entities.app.Account.API.MISSKEY) {
                        app.fedilab.android.misskey.client.endpoints.MisskeyService misskeyService =
                                new retrofit2.Retrofit.Builder()
                                        .baseUrl("https://" + java.net.IDN.toASCII(currentInstance, java.net.IDN.ALLOW_UNASSIGNED) + "/api/")
                                        .addConverterFactory(retrofit2.converter.gson.GsonConverterFactory.create(Helper.getDateBuilder()))
                                        .client(Helper.myOkHttpClient(BaseMainActivity.this))
                                        .build()
                                        .create(app.fedilab.android.misskey.client.endpoints.MisskeyService.class);
                        retrofit2.Response<app.fedilab.android.misskey.client.endpoints.MisskeyService.MisskeyEmojisResponse> response =
                                misskeyService.getEmojis().execute();
                        if (response.isSuccessful() && response.body() != null && response.body().emojis != null) {
                            List<app.fedilab.android.mastodon.client.entities.api.Emoji> emojiList = new java.util.ArrayList<>();
                            for (app.fedilab.android.misskey.client.entities.MisskeyEmoji misskeyEmoji : response.body().emojis) {
                                emojiList.add(misskeyEmoji.toEmoji());
                            }
                            emojis.put(currentInstance, emojiList);
                        }
                    } else {
                        emojis.put(currentInstance, new EmojiInstance(BaseMainActivity.this).getEmojiList(BaseMainActivity.currentInstance));
                } catch (DBException e) {
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
+91 −44
Original line number Diff line number Diff line
@@ -47,6 +47,9 @@ import app.fedilab.android.mastodon.helper.MastodonHelper;
import app.fedilab.android.mastodon.viewmodel.mastodon.AccountsVM;
import app.fedilab.android.mastodon.viewmodel.mastodon.AdminVM;
import app.fedilab.android.mastodon.viewmodel.mastodon.OauthVM;
import app.fedilab.android.misskey.helper.MisskeyHelper;
import app.fedilab.android.misskey.viewmodel.MisskeyAccountsVM;
import app.fedilab.android.misskey.viewmodel.MisskeyOauthVM;
import app.fedilab.android.ui.fragment.FragmentLoginMain;
import es.dmoral.toasty.Toasty;

@@ -57,6 +60,7 @@ public class LoginActivity extends BaseActivity {
    public static Account.API apiLogin;
    public static String currentInstanceLogin, client_idLogin, client_secretLogin, softwareLogin;
    public static boolean requestedAdmin;
    public static String misskeySessionId;


    @SuppressLint("ApplySharedPref")
@@ -68,7 +72,7 @@ public class LoginActivity extends BaseActivity {
                Handler mainHandler = new Handler(Looper.getMainLooper());
                BaseMainActivity.currentToken = account.token;
                BaseMainActivity.currentUserID = account.user_id;
                BaseMainActivity.api = Account.API.MASTODON;
                BaseMainActivity.api = account.api != null ? account.api : Account.API.MASTODON;
                SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(activity);
                SharedPreferences.Editor editor = sharedpreferences.edit();
                editor.putString(Helper.PREF_USER_TOKEN, account.token);
@@ -88,9 +92,19 @@ public class LoginActivity extends BaseActivity {
    }

    private void manageItent(Intent intent) {

        if (intent != null && intent.getData() != null && intent.getData().toString().contains(MastodonHelper.REDIRECT_CONTENT_WEB + "?code=")) {
        if (intent == null || intent.getData() == null) {
            return;
        }
        String url = intent.getData().toString();

        if (url.contains(MisskeyHelper.REDIRECT_URI)) {
            manageMisskeyCallback(url);
        } else if (url.contains(MastodonHelper.REDIRECT_CONTENT_WEB + "?code=")) {
            manageMastodonCallback(url);
        }
    }

    private void manageMastodonCallback(String url) {
        Matcher matcher = Helper.codePattern.matcher(url);
        if (!matcher.find()) {
            Toasty.error(LoginActivity.this, getString(R.string.toast_code_error), Toast.LENGTH_LONG).show();
@@ -98,8 +112,6 @@ public class LoginActivity extends BaseActivity {
        }
        String code = matcher.group(1);
        OauthVM oauthVM = new ViewModelProvider(LoginActivity.this).get(OauthVM.class);
            //We are dealing with a Mastodon API
            //API call to get the user token
        String scope = requestedAdmin ? Helper.OAUTH_SCOPES_ADMIN : Helper.OAUTH_SCOPES;
        oauthVM.createToken(currentInstanceLogin, "authorization_code", client_idLogin, client_secretLogin, Helper.REDIRECT_CONTENT_WEB, scope, code)
                .observe(LoginActivity.this, tokenObj -> {
@@ -111,13 +123,11 @@ public class LoginActivity extends BaseActivity {
                        account.api = apiLogin;
                        account.software = softwareLogin;
                        account.instance = currentInstanceLogin;
                            //API call to retrieve account information for the new token
                        AccountsVM accountsVM = new ViewModelProvider(LoginActivity.this).get(AccountsVM.class);
                        accountsVM.getConnectedAccount(currentInstanceLogin, account.token).observe(LoginActivity.this, mastodonAccount -> {
                            if (mastodonAccount != null) {
                                account.mastodon_account = mastodonAccount;
                                account.user_id = mastodonAccount.id;
                                    //We check if user have really moderator rights
                                if (requestedAdmin) {
                                    AdminVM adminVM = new ViewModelProvider(LoginActivity.this).get(AdminVM.class);
                                    adminVM.getAccount(account.instance, account.token, account.user_id).observe(LoginActivity.this, adminAccount -> {
@@ -130,14 +140,51 @@ public class LoginActivity extends BaseActivity {
                            } else {
                                Toasty.error(LoginActivity.this, getString(R.string.toast_token), Toast.LENGTH_LONG).show();
                            }

                        });
                    } else {
                        Toasty.error(LoginActivity.this, getString(R.string.toast_token), Toast.LENGTH_LONG).show();
                    }
                });
    }

    private void manageMisskeyCallback(String url) {
        Matcher sessionMatcher = Helper.sessionPattern.matcher(url);
        if (!sessionMatcher.find()) {
            Toasty.error(LoginActivity.this, getString(R.string.toast_code_error), Toast.LENGTH_LONG).show();
            return;
        }
        String session = sessionMatcher.group(1);

        if (misskeySessionId != null && !misskeySessionId.equals(session)) {
            Toasty.error(LoginActivity.this, getString(R.string.toast_error), Toast.LENGTH_LONG).show();
            return;
        }

        MisskeyOauthVM misskeyOauthVM = new ViewModelProvider(LoginActivity.this).get(MisskeyOauthVM.class);
        misskeyOauthVM.checkMiAuth(currentInstanceLogin, session).observe(LoginActivity.this, misskeyToken -> {
            if (misskeyToken != null && misskeyToken.token != null) {
                Account account = new Account();
                account.client_id = "";
                account.client_secret = "";
                account.token = misskeyToken.token;
                account.api = Account.API.MISSKEY;
                account.software = softwareLogin;
                account.instance = currentInstanceLogin;

                MisskeyAccountsVM misskeyAccountsVM = new ViewModelProvider(LoginActivity.this).get(MisskeyAccountsVM.class);
                misskeyAccountsVM.verifyCredentials(currentInstanceLogin, account.token).observe(LoginActivity.this, mastodonAccount -> {
                    if (mastodonAccount != null) {
                        account.mastodon_account = mastodonAccount;
                        account.user_id = mastodonAccount.id;
                        proceedLogin(LoginActivity.this, account);
                    } else {
                        Toasty.error(LoginActivity.this, getString(R.string.toast_token), Toast.LENGTH_LONG).show();
                    }
                });
            } else {
                Toasty.error(LoginActivity.this, getString(R.string.toast_token), Toast.LENGTH_LONG).show();
            }
        });
    }

    @Override
+17 −1
Original line number Diff line number Diff line
@@ -21,6 +21,9 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;

import android.view.View;

import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.databinding.ActivityActionsBinding;
import app.fedilab.android.mastodon.client.entities.app.Timeline;
@@ -34,6 +37,7 @@ public class ActionActivity extends BaseBarActivity {

    private ActivityActionsBinding binding;
    private boolean canGoBack;
    private boolean isMisskey;
    private FragmentMastodonTimeline fragmentMastodonTimeline;
    private FragmentMastodonAccount fragmentMastodonAccount;
    private FragmentMastodonDomainBlock fragmentMastodonDomainBlock;
@@ -49,12 +53,24 @@ public class ActionActivity extends BaseBarActivity {
            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        }
        canGoBack = false;

        app.fedilab.android.mastodon.client.entities.app.Account.API currentApi = BaseMainActivity.api;
        if (currentApi == null && Helper.getCurrentAccount(ActionActivity.this) != null) {
            currentApi = Helper.getCurrentAccount(ActionActivity.this).api;
        }
        isMisskey = currentApi == app.fedilab.android.mastodon.client.entities.app.Account.API.MISSKEY;

        binding.favourites.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.FAVOURITE_TIMELINE));
        binding.bookmarks.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.BOOKMARK_TIMELINE));
        binding.muted.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.MUTED_TIMELINE));
        binding.blocked.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.BLOCKED_TIMELINE));
        binding.domainBlock.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.BLOCKED_DOMAIN_TIMELINE));
        binding.mutedHome.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.MUTED_TIMELINE_HOME));

        if (isMisskey) {
            binding.domainBlock.setVisibility(View.GONE);
            binding.favourites.setText(R.string.reactions);
        }
    }

    private void displayTimeline(Timeline.TimeLineEnum type) {
@@ -102,7 +118,7 @@ public class ActionActivity extends BaseBarActivity {
        }
        switch (type) {
            case MUTED_TIMELINE -> setTitle(R.string.muted_menu);
            case FAVOURITE_TIMELINE -> setTitle(R.string.favourite);
            case FAVOURITE_TIMELINE -> setTitle(isMisskey ? R.string.reactions : R.string.favourite);
            case BLOCKED_TIMELINE -> setTitle(R.string.blocked_menu);
            case BOOKMARK_TIMELINE -> setTitle(R.string.bookmarks);
            case BLOCKED_DOMAIN_TIMELINE -> setTitle(R.string.blocked_domains);
Loading