Support domain redirects via `/.well-known/host-meta`

Closes #312
This commit is contained in:
Grishka 2022-11-15 15:16:58 +04:00
parent 79d99a9484
commit a336f6be89
1 changed files with 123 additions and 25 deletions

View File

@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.onboarding; package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
@ -9,6 +10,7 @@ import android.os.LocaleList;
import android.text.Editable; import android.text.Editable;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -21,6 +23,7 @@ import android.widget.RadioButton;
import android.widget.TextView; import android.widget.TextView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.MastodonErrorResponse; import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.api.requests.instance.GetInstance;
@ -36,7 +39,13 @@ import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.tabs.TabLayout; import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels; import org.parceler.Parcels;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import java.io.IOException;
import java.net.IDN; import java.net.IDN;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -46,6 +55,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilderFactory;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -58,6 +69,9 @@ import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter; import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
import okhttp3.Call;
import okhttp3.Request;
import okhttp3.Response;
public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstance>{ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstance>{
private InstancesAdapter adapter; private InstancesAdapter adapter;
@ -75,9 +89,11 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
private List<CatalogCategory> categories=new ArrayList<>(); private List<CatalogCategory> categories=new ArrayList<>();
private String loadingInstanceDomain; private String loadingInstanceDomain;
private GetInstance loadingInstanceRequest; private GetInstance loadingInstanceRequest;
private Call loadingInstanceRedirectRequest;
private HashMap<String, Instance> instancesCache=new HashMap<>(); private HashMap<String, Instance> instancesCache=new HashMap<>();
private ProgressDialog instanceProgressDialog; private ProgressDialog instanceProgressDialog;
private View buttonBar; private View buttonBar;
private HashMap<String, String> redirects=new HashMap<>(), redirectsInverse=new HashMap<>();
private boolean isSignup; private boolean isSignup;
@ -271,7 +287,7 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
}else{ }else{
showProgressDialog(); showProgressDialog();
if(!domain.equals(loadingInstanceDomain)){ if(!domain.equals(loadingInstanceDomain)){
loadInstanceInfo(domain); loadInstanceInfo(domain, false);
} }
} }
} }
@ -353,7 +369,7 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery)); Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery));
if(instance==null){ if(instance==null){
showProgressDialog(); showProgressDialog();
loadInstanceInfo(currentSearchQuery); loadInstanceInfo(currentSearchQuery, false);
}else{ }else{
proceedWithAuthOrSignup(instance); proceedWithAuthOrSignup(instance);
} }
@ -363,7 +379,7 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
private void onSearchChangedDebounced(){ private void onSearchChangedDebounced(){
currentSearchQuery=searchEdit.getText().toString().toLowerCase(); currentSearchQuery=searchEdit.getText().toString().toLowerCase();
updateFilteredList(); updateFilteredList();
loadInstanceInfo(currentSearchQuery); loadInstanceInfo(currentSearchQuery, false);
} }
private void updateFilteredList(){ private void updateFilteredList(){
@ -403,13 +419,7 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
private void showProgressDialog(){ private void showProgressDialog(){
instanceProgressDialog=new ProgressDialog(getActivity()); instanceProgressDialog=new ProgressDialog(getActivity());
instanceProgressDialog.setMessage(getString(R.string.loading_instance)); instanceProgressDialog.setMessage(getString(R.string.loading_instance));
instanceProgressDialog.setOnCancelListener(dialog->{ instanceProgressDialog.setOnCancelListener(dialog->cancelLoadingInstanceInfo());
if(loadingInstanceRequest!=null){
loadingInstanceRequest.cancel();
loadingInstanceRequest=null;
loadingInstanceDomain=null;
}
});
instanceProgressDialog.show(); instanceProgressDialog.show();
} }
@ -429,10 +439,12 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
}catch(IllegalArgumentException x){ }catch(IllegalArgumentException x){
return null; return null;
} }
if(redirects.containsKey(domain))
return redirects.get(domain);
return domain; return domain;
} }
private void loadInstanceInfo(String _domain){ private void loadInstanceInfo(String _domain, boolean isFromRedirect){
String domain=normalizeInstanceDomain(_domain); String domain=normalizeInstanceDomain(_domain);
Instance cachedInstance=instancesCache.get(domain); Instance cachedInstance=instancesCache.get(domain);
if(cachedInstance!=null){ if(cachedInstance!=null){
@ -446,10 +458,11 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
return; return;
} }
if(loadingInstanceDomain!=null){ if(loadingInstanceDomain!=null){
if(loadingInstanceDomain.equals(domain)) if(loadingInstanceDomain.equals(domain)){
return; return;
else }else{
loadingInstanceRequest.cancel(); cancelLoadingInstanceInfo();
}
} }
loadingInstanceDomain=domain; loadingInstanceDomain=domain;
loadingInstanceRequest=new GetInstance(); loadingInstanceRequest=new GetInstance();
@ -465,7 +478,7 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
instanceProgressDialog=null; instanceProgressDialog=null;
proceedWithAuthOrSignup(result); proceedWithAuthOrSignup(result);
} }
if(domain.equals(currentSearchQuery)){ if(domain.equals(currentSearchQuery) || currentSearchQuery.equals(redirects.get(domain)) || currentSearchQuery.equals(redirectsInverse.get(domain))){
boolean found=false; boolean found=false;
for(CatalogInstance ci:filteredData){ for(CatalogInstance ci:filteredData){
if(ci.domain.equals(domain)){ if(ci.domain.equals(domain)){
@ -484,20 +497,105 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error){
loadingInstanceRequest=null; loadingInstanceRequest=null;
loadingInstanceDomain=null; if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){
if(instanceProgressDialog!=null){ fetchDomainFromHostMetaAndMaybeRetry(domain, error);
instanceProgressDialog.dismiss(); return;
instanceProgressDialog=null;
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(getString(R.string.not_a_mastodon_instance, domain)+"\n\n"+((MastodonErrorResponse)error).error)
.setPositiveButton(R.string.ok, null)
.show();
} }
loadingInstanceDomain=null;
showInstanceInfoLoadError(domain, error);
} }
}).execNoAuth(domain); }).execNoAuth(domain);
} }
private void cancelLoadingInstanceInfo(){
if(loadingInstanceRequest!=null){
loadingInstanceRequest.cancel();
loadingInstanceRequest=null;
}
if(loadingInstanceRedirectRequest!=null){
loadingInstanceRedirectRequest.cancel();
loadingInstanceRedirectRequest=null;
}
loadingInstanceDomain=null;
if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss();
instanceProgressDialog=null;
}
}
private void showInstanceInfoLoadError(String domain, Object error){
if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss();
instanceProgressDialog=null;
String additionalInfo;
if(error instanceof MastodonErrorResponse me){
additionalInfo="\n\n"+me.error;
}else if(error instanceof Throwable t){
additionalInfo="\n\n"+t.getLocalizedMessage();
}else{
additionalInfo="";
}
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(getString(R.string.not_a_mastodon_instance, domain)+additionalInfo)
.setPositiveButton(R.string.ok, null)
.show();
}
}
private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError){
String url="https://"+domain+"/.well-known/host-meta";
Request req=new Request.Builder()
.url(url)
.build();
loadingInstanceRedirectRequest=MastodonAPIController.getHttpClient().newCall(req);
loadingInstanceRedirectRequest.enqueue(new okhttp3.Callback(){
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e){
loadingInstanceRedirectRequest=null;
loadingInstanceDomain=null;
Activity a=getActivity();
if(a==null)
return;
a.runOnUiThread(()->showInstanceInfoLoadError(domain, e));
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
loadingInstanceRedirectRequest=null;
loadingInstanceDomain=null;
Activity a=getActivity();
if(a==null)
return;
try(response){
if(!response.isSuccessful()){
a.runOnUiThread(()->showInstanceInfoLoadError(domain, response.code()+" "+response.message()));
return;
}
InputSource source=new InputSource(response.body().charStream());
Document doc=DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(source);
NodeList list=doc.getElementsByTagName("Link");
for(int i=0;i<list.getLength();i++){
if(list.item(i) instanceof Element el){
String template=el.getAttribute("template");
if("lrdd".equals(el.getAttribute("rel")) && !TextUtils.isEmpty(template) && template.contains("{uri}")){
Uri uri=Uri.parse(template.replace("{uri}", "qwe"));
String redirectDomain=normalizeInstanceDomain(uri.getHost());
redirects.put(domain, redirectDomain);
redirectsInverse.put(redirectDomain, domain);
a.runOnUiThread(()->loadInstanceInfo(redirectDomain, true));
return;
}
}
}
a.runOnUiThread(()->showInstanceInfoLoadError(domain, origError));
}catch(Exception x){
a.runOnUiThread(()->showInstanceInfoLoadError(domain, x));
}
}
});
}
@Override @Override
public void onApplyWindowInsets(WindowInsets insets){ public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){ if(Build.VERSION.SDK_INT>=27){
@ -580,7 +678,7 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
if(chosenInstance==null) if(chosenInstance==null)
nextButton.setEnabled(true); nextButton.setEnabled(true);
chosenInstance=item; chosenInstance=item;
loadInstanceInfo(chosenInstance.domain); loadInstanceInfo(chosenInstance.domain, false);
} }
} }
} }