mirror of
				https://github.com/KokaKiwi/BarInsta
				synced 2025-10-30 19:15:35 +00:00 
			
		
		
		
	Merge branch 'master' of https://github.com/austinhuang0131/instagrabber
This commit is contained in:
		
						commit
						7f7db43870
					
				| @ -147,6 +147,11 @@ android { | ||||
|         exclude 'META-INF/LICENSE.md' | ||||
|         exclude 'META-INF/LICENSE-notice.md' | ||||
|     } | ||||
| 
 | ||||
|     testOptions.unitTests { | ||||
|         includeAndroidResources = true | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| configurations.all { | ||||
| @ -190,6 +195,7 @@ dependencies { | ||||
|     def room_version = "2.3.0" | ||||
|     implementation "androidx.room:room-runtime:$room_version" | ||||
|     implementation "androidx.room:room-guava:$room_version" | ||||
|     implementation "androidx.room:room-ktx:$room_version" | ||||
|     annotationProcessor "androidx.room:room-compiler:$room_version" | ||||
| 
 | ||||
|     // CameraX | ||||
| @ -231,6 +237,9 @@ dependencies { | ||||
|     githubImplementation 'io.sentry:sentry-android:4.3.0' | ||||
| 
 | ||||
|     testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2' | ||||
|     testImplementation "androidx.test.ext:junit-ktx:1.1.2" | ||||
|     testImplementation "androidx.test:core-ktx:1.3.0" | ||||
|     testImplementation "org.robolectric:robolectric:4.5.1" | ||||
| 
 | ||||
|     androidTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.1' | ||||
|     androidTestImplementation 'androidx.test:core:1.3.0' | ||||
|  | ||||
| @ -49,7 +49,6 @@ import awais.instagrabber.models.IntentModel | ||||
| import awais.instagrabber.models.Resource | ||||
| import awais.instagrabber.models.Tab | ||||
| import awais.instagrabber.models.enums.IntentModelType | ||||
| import awais.instagrabber.repositories.responses.Media | ||||
| import awais.instagrabber.services.ActivityCheckerService | ||||
| import awais.instagrabber.services.DMSyncAlarmReceiver | ||||
| import awais.instagrabber.utils.* | ||||
| @ -61,7 +60,6 @@ import awais.instagrabber.viewmodels.AppStateViewModel | ||||
| import awais.instagrabber.viewmodels.DirectInboxViewModel | ||||
| import awais.instagrabber.webservices.GraphQLService | ||||
| import awais.instagrabber.webservices.MediaService | ||||
| import awais.instagrabber.webservices.ServiceCallback | ||||
| import com.google.android.material.appbar.AppBarLayout | ||||
| import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior | ||||
| import com.google.android.material.appbar.CollapsingToolbarLayout | ||||
| @ -71,6 +69,7 @@ import com.google.common.collect.ImmutableList | ||||
| import com.google.common.collect.Iterators | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.withContext | ||||
| import java.util.* | ||||
| import java.util.stream.Collectors | ||||
| 
 | ||||
| @ -92,8 +91,6 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL | ||||
|     var currentTabs: List<Tab> = emptyList() | ||||
|         private set | ||||
|     private var showBottomViewDestinations: List<Int> = emptyList() | ||||
|     private var graphQLService: GraphQLService? = null | ||||
|     private var mediaService: MediaService? = null | ||||
| 
 | ||||
|     private val serviceConnection: ServiceConnection = object : ServiceConnection { | ||||
|         override fun onServiceConnected(name: ComponentName, service: IBinder) { | ||||
| @ -637,42 +634,32 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL | ||||
|             .setCancelable(false) | ||||
|             .setView(R.layout.dialog_opening_post) | ||||
|             .create() | ||||
|         if (graphQLService == null) graphQLService = GraphQLService.getInstance() | ||||
|         if (mediaService == null) { | ||||
|             mediaService = deviceUuid?.let { csrfToken?.let { it1 -> MediaService.getInstance(it, it1, userId) } } | ||||
|         } | ||||
|         val postCb: ServiceCallback<Media> = object : ServiceCallback<Media> { | ||||
|             override fun onSuccess(feedModel: Media?) { | ||||
|                 if (feedModel != null) { | ||||
|                     val currentNavControllerLiveData = currentNavControllerLiveData ?: return | ||||
|         alertDialog.show() | ||||
|         lifecycleScope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val media = if (isLoggedIn) MediaService.fetch(shortcodeToId(shortCode)) else GraphQLService.fetchPost(shortCode) | ||||
|                 withContext(Dispatchers.Main) { | ||||
|                     if (media == null) { | ||||
|                         Toast.makeText(applicationContext, R.string.post_not_found, Toast.LENGTH_SHORT).show() | ||||
|                         return@withContext | ||||
|                     } | ||||
|                     val currentNavControllerLiveData = currentNavControllerLiveData ?: return@withContext | ||||
|                     val navController = currentNavControllerLiveData.value | ||||
|                     val bundle = Bundle() | ||||
|                     bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, feedModel) | ||||
|                     bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, media) | ||||
|                     try { | ||||
|                         navController?.navigate(R.id.action_global_post_view, bundle) | ||||
|                     } catch (e: Exception) { | ||||
|                         Log.e(TAG, "showPostView: ", e) | ||||
|                     } | ||||
|                 } else Toast.makeText(applicationContext, R.string.post_not_found, Toast.LENGTH_SHORT).show() | ||||
|                 alertDialog.dismiss() | ||||
|             } | ||||
| 
 | ||||
|             override fun onFailure(t: Throwable) { | ||||
|                 alertDialog.dismiss() | ||||
|             } | ||||
|         } | ||||
|         alertDialog.show() | ||||
|         if (isLoggedIn) { | ||||
|             lifecycleScope.launch(Dispatchers.IO) { | ||||
|                 try { | ||||
|                     val media = mediaService?.fetch(shortcodeToId(shortCode)) | ||||
|                     postCb.onSuccess(media) | ||||
|                 } catch (e: Exception) { | ||||
|                     postCb.onFailure(e) | ||||
|                 } | ||||
|             } catch (e: Exception) { | ||||
|                 Log.e(TAG, "showPostView: ", e) | ||||
|             } finally { | ||||
|                 withContext(Dispatchers.Main) { | ||||
|                     alertDialog.dismiss() | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             graphQLService?.fetchPost(shortCode, postCb) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -7,9 +7,11 @@ import awais.instagrabber.interfaces.FetchListener; | ||||
| import awais.instagrabber.repositories.responses.Hashtag; | ||||
| import awais.instagrabber.repositories.responses.Media; | ||||
| import awais.instagrabber.repositories.responses.PostsFetchResponse; | ||||
| import awais.instagrabber.utils.CoroutineUtilsKt; | ||||
| import awais.instagrabber.webservices.GraphQLService; | ||||
| import awais.instagrabber.webservices.ServiceCallback; | ||||
| import awais.instagrabber.webservices.TagsService; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| 
 | ||||
| public class HashtagPostFetchService implements PostFetcher.PostFetchService { | ||||
|     private final TagsService tagsService; | ||||
| @ -23,7 +25,7 @@ public class HashtagPostFetchService implements PostFetcher.PostFetchService { | ||||
|         this.hashtagModel = hashtagModel; | ||||
|         this.isLoggedIn = isLoggedIn; | ||||
|         tagsService = isLoggedIn ? TagsService.getInstance() : null; | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @ -48,7 +50,17 @@ public class HashtagPostFetchService implements PostFetcher.PostFetchService { | ||||
|             } | ||||
|         }; | ||||
|         if (isLoggedIn) tagsService.fetchPosts(hashtagModel.getName().toLowerCase(), nextMaxId, cb); | ||||
|         else graphQLService.fetchHashtagPosts(hashtagModel.getName().toLowerCase(), nextMaxId, cb); | ||||
|         else graphQLService.fetchHashtagPosts( | ||||
|                 hashtagModel.getName().toLowerCase(), | ||||
|                 nextMaxId, | ||||
|                 CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> { | ||||
|                     if (throwable != null) { | ||||
|                         cb.onFailure(throwable); | ||||
|                         return; | ||||
|                     } | ||||
|                     cb.onSuccess(postsFetchResponse); | ||||
|                 }, Dispatchers.getIO()) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | ||||
| @ -7,9 +7,11 @@ import awais.instagrabber.interfaces.FetchListener; | ||||
| import awais.instagrabber.repositories.responses.Location; | ||||
| import awais.instagrabber.repositories.responses.Media; | ||||
| import awais.instagrabber.repositories.responses.PostsFetchResponse; | ||||
| import awais.instagrabber.utils.CoroutineUtilsKt; | ||||
| import awais.instagrabber.webservices.GraphQLService; | ||||
| import awais.instagrabber.webservices.LocationService; | ||||
| import awais.instagrabber.webservices.ServiceCallback; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| 
 | ||||
| public class LocationPostFetchService implements PostFetcher.PostFetchService { | ||||
|     private final LocationService locationService; | ||||
| @ -23,7 +25,7 @@ public class LocationPostFetchService implements PostFetcher.PostFetchService { | ||||
|         this.locationModel = locationModel; | ||||
|         this.isLoggedIn = isLoggedIn; | ||||
|         locationService = isLoggedIn ? LocationService.getInstance() : null; | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @ -48,7 +50,17 @@ public class LocationPostFetchService implements PostFetcher.PostFetchService { | ||||
|             } | ||||
|         }; | ||||
|         if (isLoggedIn) locationService.fetchPosts(locationModel.getPk(), nextMaxId, cb); | ||||
|         else graphQLService.fetchLocationPosts(locationModel.getPk(), nextMaxId, cb); | ||||
|         else graphQLService.fetchLocationPosts( | ||||
|                 locationModel.getPk(), | ||||
|                 nextMaxId, | ||||
|                 CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> { | ||||
|                     if (throwable != null) { | ||||
|                         cb.onFailure(throwable); | ||||
|                         return; | ||||
|                     } | ||||
|                     cb.onSuccess(postsFetchResponse); | ||||
|                 }, Dispatchers.getIO()) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | ||||
| @ -7,9 +7,11 @@ import awais.instagrabber.interfaces.FetchListener; | ||||
| import awais.instagrabber.repositories.responses.Media; | ||||
| import awais.instagrabber.repositories.responses.PostsFetchResponse; | ||||
| import awais.instagrabber.repositories.responses.User; | ||||
| import awais.instagrabber.utils.CoroutineUtilsKt; | ||||
| import awais.instagrabber.webservices.GraphQLService; | ||||
| import awais.instagrabber.webservices.ProfileService; | ||||
| import awais.instagrabber.webservices.ServiceCallback; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| 
 | ||||
| public class ProfilePostFetchService implements PostFetcher.PostFetchService { | ||||
|     private static final String TAG = "ProfilePostFetchService"; | ||||
| @ -23,7 +25,7 @@ public class ProfilePostFetchService implements PostFetcher.PostFetchService { | ||||
|     public ProfilePostFetchService(final User profileModel, final boolean isLoggedIn) { | ||||
|         this.profileModel = profileModel; | ||||
|         this.isLoggedIn = isLoggedIn; | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; | ||||
|         profileService = isLoggedIn ? ProfileService.getInstance() : null; | ||||
|     } | ||||
| 
 | ||||
| @ -49,7 +51,19 @@ public class ProfilePostFetchService implements PostFetcher.PostFetchService { | ||||
|             } | ||||
|         }; | ||||
|         if (isLoggedIn) profileService.fetchPosts(profileModel.getPk(), nextMaxId, cb); | ||||
|         else graphQLService.fetchProfilePosts(profileModel.getPk(), 30, nextMaxId, profileModel, cb); | ||||
|         else graphQLService.fetchProfilePosts( | ||||
|                 profileModel.getPk(), | ||||
|                 30, | ||||
|                 nextMaxId, | ||||
|                 profileModel, | ||||
|                 CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> { | ||||
|                     if (throwable != null) { | ||||
|                         cb.onFailure(throwable); | ||||
|                         return; | ||||
|                     } | ||||
|                     cb.onSuccess(postsFetchResponse); | ||||
|                 }, Dispatchers.getIO()) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | ||||
| @ -7,9 +7,11 @@ import awais.instagrabber.interfaces.FetchListener; | ||||
| import awais.instagrabber.models.enums.PostItemType; | ||||
| import awais.instagrabber.repositories.responses.Media; | ||||
| import awais.instagrabber.repositories.responses.PostsFetchResponse; | ||||
| import awais.instagrabber.utils.CoroutineUtilsKt; | ||||
| import awais.instagrabber.webservices.GraphQLService; | ||||
| import awais.instagrabber.webservices.ProfileService; | ||||
| import awais.instagrabber.webservices.ServiceCallback; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| 
 | ||||
| public class SavedPostFetchService implements PostFetcher.PostFetchService { | ||||
|     private final ProfileService profileService; | ||||
| @ -27,7 +29,7 @@ public class SavedPostFetchService implements PostFetcher.PostFetchService { | ||||
|         this.type = type; | ||||
|         this.isLoggedIn = isLoggedIn; | ||||
|         this.collectionId = collectionId; | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; | ||||
|         profileService = isLoggedIn ? ProfileService.getInstance() : null; | ||||
|     } | ||||
| 
 | ||||
| @ -58,7 +60,18 @@ public class SavedPostFetchService implements PostFetcher.PostFetchService { | ||||
|                 break; | ||||
|             case TAGGED: | ||||
|                 if (isLoggedIn) profileService.fetchTagged(profileId, nextMaxId, callback); | ||||
|                 else graphQLService.fetchTaggedPosts(profileId, 30, nextMaxId, callback); | ||||
|                 else graphQLService.fetchTaggedPosts( | ||||
|                         profileId, | ||||
|                         30, | ||||
|                         nextMaxId, | ||||
|                         CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> { | ||||
|                             if (throwable != null) { | ||||
|                                 callback.onFailure(throwable); | ||||
|                                 return; | ||||
|                             } | ||||
|                             callback.onSuccess(postsFetchResponse); | ||||
|                         }, Dispatchers.getIO()) | ||||
|                 ); | ||||
|                 break; | ||||
|             case COLLECTION: | ||||
|             case SAVED: | ||||
|  | ||||
| @ -1,34 +0,0 @@ | ||||
| package awais.instagrabber.db.dao; | ||||
| 
 | ||||
| import androidx.room.Dao; | ||||
| import androidx.room.Delete; | ||||
| import androidx.room.Insert; | ||||
| import androidx.room.OnConflictStrategy; | ||||
| import androidx.room.Query; | ||||
| import androidx.room.Update; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| import awais.instagrabber.db.entities.Account; | ||||
| 
 | ||||
| @Dao | ||||
| public interface AccountDao { | ||||
| 
 | ||||
|     @Query("SELECT * FROM accounts") | ||||
|     List<Account> getAllAccounts(); | ||||
| 
 | ||||
|     @Query("SELECT * FROM accounts WHERE uid = :uid") | ||||
|     Account findAccountByUid(String uid); | ||||
| 
 | ||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||
|     List<Long> insertAccounts(Account... accounts); | ||||
| 
 | ||||
|     @Update | ||||
|     void updateAccounts(Account... accounts); | ||||
| 
 | ||||
|     @Delete | ||||
|     void deleteAccounts(Account... accounts); | ||||
| 
 | ||||
|     @Query("DELETE from accounts") | ||||
|     void deleteAllAccounts(); | ||||
| } | ||||
							
								
								
									
										25
									
								
								app/src/main/java/awais/instagrabber/db/dao/AccountDao.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/src/main/java/awais/instagrabber/db/dao/AccountDao.kt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| package awais.instagrabber.db.dao | ||||
| 
 | ||||
| import androidx.room.* | ||||
| import awais.instagrabber.db.entities.Account | ||||
| 
 | ||||
| @Dao | ||||
| interface AccountDao { | ||||
|     @Query("SELECT * FROM accounts") | ||||
|     suspend fun getAllAccounts(): List<Account> | ||||
| 
 | ||||
|     @Query("SELECT * FROM accounts WHERE uid = :uid") | ||||
|     suspend fun findAccountByUid(uid: String): Account? | ||||
| 
 | ||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||
|     suspend fun insertAccounts(vararg accounts: Account): List<Long> | ||||
| 
 | ||||
|     @Update | ||||
|     suspend fun updateAccounts(vararg accounts: Account) | ||||
| 
 | ||||
|     @Delete | ||||
|     suspend fun deleteAccounts(vararg accounts: Account) | ||||
| 
 | ||||
|     @Query("DELETE from accounts") | ||||
|     suspend fun deleteAllAccounts() | ||||
| } | ||||
| @ -1,68 +0,0 @@ | ||||
| package awais.instagrabber.db.datasources; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| import awais.instagrabber.db.AppDatabase; | ||||
| import awais.instagrabber.db.dao.AccountDao; | ||||
| import awais.instagrabber.db.entities.Account; | ||||
| 
 | ||||
| public class AccountDataSource { | ||||
|     private static final String TAG = AccountDataSource.class.getSimpleName(); | ||||
| 
 | ||||
|     private static AccountDataSource INSTANCE; | ||||
| 
 | ||||
|     private final AccountDao accountDao; | ||||
| 
 | ||||
|     private AccountDataSource(final AccountDao accountDao) { | ||||
|         this.accountDao = accountDao; | ||||
|     } | ||||
| 
 | ||||
|     public static AccountDataSource getInstance(@NonNull Context context) { | ||||
|         if (INSTANCE == null) { | ||||
|             synchronized (AccountDataSource.class) { | ||||
|                 if (INSTANCE == null) { | ||||
|                     final AppDatabase database = AppDatabase.getDatabase(context); | ||||
|                     INSTANCE = new AccountDataSource(database.accountDao()); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return INSTANCE; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public final Account getAccount(final String uid) { | ||||
|         return accountDao.findAccountByUid(uid); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public final List<Account> getAllAccounts() { | ||||
|         return accountDao.getAllAccounts(); | ||||
|     } | ||||
| 
 | ||||
|     public final void insertOrUpdateAccount(final String uid, | ||||
|                                             final String username, | ||||
|                                             final String cookie, | ||||
|                                             final String fullName, | ||||
|                                             final String profilePicUrl) { | ||||
|         final Account account = getAccount(uid); | ||||
|         final Account toUpdate = new Account(account == null ? 0 : account.getId(), uid, username, cookie, fullName, profilePicUrl); | ||||
|         if (account != null) { | ||||
|             accountDao.updateAccounts(toUpdate); | ||||
|             return; | ||||
|         } | ||||
|         accountDao.insertAccounts(toUpdate); | ||||
|     } | ||||
| 
 | ||||
|     public final void deleteAccount(@NonNull final Account account) { | ||||
|         accountDao.deleteAccounts(account); | ||||
|     } | ||||
| 
 | ||||
|     public final void deleteAllAccounts() { | ||||
|         accountDao.deleteAllAccounts(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,49 @@ | ||||
| package awais.instagrabber.db.datasources | ||||
| 
 | ||||
| import android.content.Context | ||||
| import awais.instagrabber.db.AppDatabase | ||||
| import awais.instagrabber.db.dao.AccountDao | ||||
| import awais.instagrabber.db.entities.Account | ||||
| 
 | ||||
| class AccountDataSource private constructor(private val accountDao: AccountDao) { | ||||
|     suspend fun getAccount(uid: String): Account? = accountDao.findAccountByUid(uid) | ||||
| 
 | ||||
|     suspend fun getAllAccounts(): List<Account> = accountDao.getAllAccounts() | ||||
| 
 | ||||
|     suspend fun insertOrUpdateAccount( | ||||
|         uid: String, | ||||
|         username: String, | ||||
|         cookie: String, | ||||
|         fullName: String, | ||||
|         profilePicUrl: String?, | ||||
|     ) { | ||||
|         val account = getAccount(uid) | ||||
|         val toUpdate = Account(account?.id ?: 0, uid, username, cookie, fullName, profilePicUrl) | ||||
|         if (account != null) { | ||||
|             accountDao.updateAccounts(toUpdate) | ||||
|             return | ||||
|         } | ||||
|         accountDao.insertAccounts(toUpdate) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun deleteAccount(account: Account) = accountDao.deleteAccounts(account) | ||||
| 
 | ||||
|     suspend fun deleteAllAccounts() = accountDao.deleteAllAccounts() | ||||
| 
 | ||||
|     companion object { | ||||
|         private lateinit var INSTANCE: AccountDataSource | ||||
| 
 | ||||
|         @JvmStatic | ||||
|         fun getInstance(context: Context): AccountDataSource { | ||||
|             if (!this::INSTANCE.isInitialized) { | ||||
|                 synchronized(AccountDataSource::class.java) { | ||||
|                     if (!this::INSTANCE.isInitialized) { | ||||
|                         val database = AppDatabase.getDatabase(context) | ||||
|                         INSTANCE = AccountDataSource(database.accountDao()) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             return INSTANCE | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,131 +0,0 @@ | ||||
| package awais.instagrabber.db.repositories; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| import awais.instagrabber.db.datasources.AccountDataSource; | ||||
| import awais.instagrabber.db.entities.Account; | ||||
| import awais.instagrabber.utils.AppExecutors; | ||||
| 
 | ||||
| public class AccountRepository { | ||||
|     private static final String TAG = AccountRepository.class.getSimpleName(); | ||||
| 
 | ||||
|     private static AccountRepository instance; | ||||
| 
 | ||||
|     private final AppExecutors appExecutors; | ||||
|     private final AccountDataSource accountDataSource; | ||||
| 
 | ||||
|     // private List<Account> cachedAccounts; | ||||
| 
 | ||||
|     private AccountRepository(final AppExecutors appExecutors, final AccountDataSource accountDataSource) { | ||||
|         this.appExecutors = appExecutors; | ||||
|         this.accountDataSource = accountDataSource; | ||||
|     } | ||||
| 
 | ||||
|     public static AccountRepository getInstance(final AccountDataSource accountDataSource) { | ||||
|         if (instance == null) { | ||||
|             instance = new AccountRepository(AppExecutors.INSTANCE, accountDataSource); | ||||
|         } | ||||
|         return instance; | ||||
|     } | ||||
| 
 | ||||
|     public void getAccount(final long uid, | ||||
|                            final RepositoryCallback<Account> callback) { | ||||
|         // request on the I/O thread | ||||
|         appExecutors.getDiskIO().execute(() -> { | ||||
|             final Account account = accountDataSource.getAccount(String.valueOf(uid)); | ||||
|             // notify on the main thread | ||||
|             appExecutors.getMainThread().execute(() -> { | ||||
|                 if (callback == null) return; | ||||
|                 if (account == null) { | ||||
|                     callback.onDataNotAvailable(); | ||||
|                     return; | ||||
|                 } | ||||
|                 callback.onSuccess(account); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void getAllAccounts(final RepositoryCallback<List<Account>> callback) { | ||||
|         // request on the I/O thread | ||||
|         appExecutors.getDiskIO().execute(() -> { | ||||
|             final List<Account> accounts = accountDataSource.getAllAccounts(); | ||||
|             // notify on the main thread | ||||
|             appExecutors.getMainThread().execute(() -> { | ||||
|                 if (callback == null) return; | ||||
|                 if (accounts == null) { | ||||
|                     callback.onDataNotAvailable(); | ||||
|                     return; | ||||
|                 } | ||||
|                 // cachedAccounts = accounts; | ||||
|                 callback.onSuccess(accounts); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void insertOrUpdateAccounts(final List<Account> accounts, | ||||
|                                        final RepositoryCallback<Void> callback) { | ||||
|         // request on the I/O thread | ||||
|         appExecutors.getDiskIO().execute(() -> { | ||||
|             for (final Account account : accounts) { | ||||
|                 accountDataSource.insertOrUpdateAccount(account.getUid(), | ||||
|                                                         account.getUsername(), | ||||
|                                                         account.getCookie(), | ||||
|                                                         account.getFullName(), | ||||
|                                                         account.getProfilePic()); | ||||
|             } | ||||
|             // notify on the main thread | ||||
|             appExecutors.getMainThread().execute(() -> { | ||||
|                 if (callback == null) return; | ||||
|                 callback.onSuccess(null); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void insertOrUpdateAccount(final long uid, | ||||
|                                       final String username, | ||||
|                                       final String cookie, | ||||
|                                       final String fullName, | ||||
|                                       final String profilePicUrl, | ||||
|                                       final RepositoryCallback<Account> callback) { | ||||
|         // request on the I/O thread | ||||
|         appExecutors.getDiskIO().execute(() -> { | ||||
|             accountDataSource.insertOrUpdateAccount(String.valueOf(uid), username, cookie, fullName, profilePicUrl); | ||||
|             final Account updated = accountDataSource.getAccount(String.valueOf(uid)); | ||||
|             // notify on the main thread | ||||
|             appExecutors.getMainThread().execute(() -> { | ||||
|                 if (callback == null) return; | ||||
|                 if (updated == null) { | ||||
|                     callback.onDataNotAvailable(); | ||||
|                     return; | ||||
|                 } | ||||
|                 callback.onSuccess(updated); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void deleteAccount(final Account account, | ||||
|                               final RepositoryCallback<Void> callback) { | ||||
|         // request on the I/O thread | ||||
|         appExecutors.getDiskIO().execute(() -> { | ||||
|             accountDataSource.deleteAccount(account); | ||||
|             // notify on the main thread | ||||
|             appExecutors.getMainThread().execute(() -> { | ||||
|                 if (callback == null) return; | ||||
|                 callback.onSuccess(null); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void deleteAllAccounts(final RepositoryCallback<Void> callback) { | ||||
|         // request on the I/O thread | ||||
|         appExecutors.getDiskIO().execute(() -> { | ||||
|             accountDataSource.deleteAllAccounts(); | ||||
|             // notify on the main thread | ||||
|             appExecutors.getMainThread().execute(() -> { | ||||
|                 if (callback == null) return; | ||||
|                 callback.onSuccess(null); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,49 @@ | ||||
| package awais.instagrabber.db.repositories | ||||
| 
 | ||||
| import awais.instagrabber.db.datasources.AccountDataSource | ||||
| import awais.instagrabber.db.entities.Account | ||||
| 
 | ||||
| class AccountRepository private constructor(private val accountDataSource: AccountDataSource) { | ||||
|     suspend fun getAccount(uid: Long): Account? = accountDataSource.getAccount(uid.toString()) | ||||
| 
 | ||||
|     suspend fun getAllAccounts(): List<Account> = accountDataSource.getAllAccounts() | ||||
| 
 | ||||
|     suspend fun insertOrUpdateAccounts(accounts: List<Account>) { | ||||
|         for (account in accounts) { | ||||
|             accountDataSource.insertOrUpdateAccount( | ||||
|                 account.uid, | ||||
|                 account.username, | ||||
|                 account.cookie, | ||||
|                 account.fullName, | ||||
|                 account.profilePic | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     suspend fun insertOrUpdateAccount( | ||||
|         uid: Long, | ||||
|         username: String, | ||||
|         cookie: String, | ||||
|         fullName: String, | ||||
|         profilePicUrl: String?, | ||||
|     ): Account? { | ||||
|         accountDataSource.insertOrUpdateAccount(uid.toString(), username, cookie, fullName, profilePicUrl) | ||||
|         return accountDataSource.getAccount(uid.toString()) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun deleteAccount(account: Account) = accountDataSource.deleteAccount(account) | ||||
| 
 | ||||
|     suspend fun deleteAllAccounts() = accountDataSource.deleteAllAccounts() | ||||
| 
 | ||||
|     companion object { | ||||
|         private lateinit var instance: AccountRepository | ||||
| 
 | ||||
|         @JvmStatic | ||||
|         fun getInstance(accountDataSource: AccountDataSource): AccountRepository { | ||||
|             if (!this::instance.isInitialized) { | ||||
|                 instance = AccountRepository(accountDataSource) | ||||
|             } | ||||
|             return instance | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -3,6 +3,7 @@ package awais.instagrabber.dialogs; | ||||
| import android.app.Dialog; | ||||
| import android.content.Context; | ||||
| import android.os.Bundle; | ||||
| import android.util.Log; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| @ -14,6 +15,7 @@ import androidx.appcompat.app.AlertDialog; | ||||
| import androidx.fragment.app.DialogFragment; | ||||
| import androidx.recyclerview.widget.LinearLayoutManager; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| 
 | ||||
| @ -23,30 +25,29 @@ import awais.instagrabber.databinding.DialogAccountSwitcherBinding; | ||||
| import awais.instagrabber.db.datasources.AccountDataSource; | ||||
| import awais.instagrabber.db.entities.Account; | ||||
| import awais.instagrabber.db.repositories.AccountRepository; | ||||
| import awais.instagrabber.db.repositories.RepositoryCallback; | ||||
| import awais.instagrabber.utils.AppExecutors; | ||||
| import awais.instagrabber.utils.Constants; | ||||
| import awais.instagrabber.utils.CookieUtils; | ||||
| import awais.instagrabber.utils.CoroutineUtilsKt; | ||||
| import awais.instagrabber.utils.ProcessPhoenix; | ||||
| import awais.instagrabber.utils.TextUtils; | ||||
| import awais.instagrabber.utils.Utils; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| 
 | ||||
| import static awais.instagrabber.utils.Utils.settingsHelper; | ||||
| 
 | ||||
| public class AccountSwitcherDialogFragment extends DialogFragment { | ||||
|     private static final String TAG = AccountSwitcherDialogFragment.class.getSimpleName(); | ||||
| 
 | ||||
|     private AccountRepository accountRepository; | ||||
| 
 | ||||
|     private OnAddAccountClickListener onAddAccountClickListener; | ||||
|     private DialogAccountSwitcherBinding binding; | ||||
| 
 | ||||
|     public AccountSwitcherDialogFragment() { | ||||
|         accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(getContext())); | ||||
|     } | ||||
|     public AccountSwitcherDialogFragment() {} | ||||
| 
 | ||||
|     public AccountSwitcherDialogFragment(final OnAddAccountClickListener onAddAccountClickListener) { | ||||
|         this.onAddAccountClickListener = onAddAccountClickListener; | ||||
|         accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(getContext())); | ||||
|     } | ||||
| 
 | ||||
|     private final AccountSwitcherAdapter.OnAccountClickListener accountClickListener = (model, isCurrent) -> { | ||||
| @ -80,17 +81,15 @@ public class AccountSwitcherDialogFragment extends DialogFragment { | ||||
|                 .setMessage(getString(R.string.quick_access_confirm_delete, model.getUsername())) | ||||
|                 .setPositiveButton(R.string.yes, (dialog, which) -> { | ||||
|                     if (accountRepository == null) return; | ||||
|                     accountRepository.deleteAccount(model, new RepositoryCallback<Void>() { | ||||
|                         @Override | ||||
|                         public void onSuccess(final Void result) { | ||||
|                             dismiss(); | ||||
|                         } | ||||
| 
 | ||||
|                         @Override | ||||
|                         public void onDataNotAvailable() { | ||||
|                             dismiss(); | ||||
|                         } | ||||
|                     }); | ||||
|                     accountRepository.deleteAccount( | ||||
|                             model, | ||||
|                             CoroutineUtilsKt.getContinuation((unit, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                 dismiss(); | ||||
|                                 if (throwable != null) { | ||||
|                                     Log.e(TAG, "deleteAccount: ", throwable); | ||||
|                                 } | ||||
|                             }), Dispatchers.getIO()) | ||||
|                     ); | ||||
|                 }) | ||||
|                 .setNegativeButton(R.string.cancel, null) | ||||
|                 .show(); | ||||
| @ -113,6 +112,12 @@ public class AccountSwitcherDialogFragment extends DialogFragment { | ||||
|         init(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onAttach(@NonNull final Context context) { | ||||
|         super.onAttach(context); | ||||
|         accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(context)); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onStart() { | ||||
|         super.onStart(); | ||||
| @ -129,18 +134,19 @@ public class AccountSwitcherDialogFragment extends DialogFragment { | ||||
|         final AccountSwitcherAdapter adapter = new AccountSwitcherAdapter(accountClickListener, accountLongClickListener); | ||||
|         binding.accounts.setAdapter(adapter); | ||||
|         if (accountRepository == null) return; | ||||
|         accountRepository.getAllAccounts(new RepositoryCallback<List<Account>>() { | ||||
|             @Override | ||||
|             public void onSuccess(final List<Account> accounts) { | ||||
|                 if (accounts == null) return; | ||||
|                 final String cookie = settingsHelper.getString(Constants.COOKIE); | ||||
|                 sortUserList(cookie, accounts); | ||||
|                 adapter.submitList(accounts); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onDataNotAvailable() {} | ||||
|         }); | ||||
|         accountRepository.getAllAccounts( | ||||
|                 CoroutineUtilsKt.getContinuation((accounts, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                     if (throwable != null) { | ||||
|                         Log.e(TAG, "init: ", throwable); | ||||
|                         return; | ||||
|                     } | ||||
|                     if (accounts == null) return; | ||||
|                     final String cookie = settingsHelper.getString(Constants.COOKIE); | ||||
|                     final List<Account> copy = new ArrayList<>(accounts); | ||||
|                     sortUserList(cookie, copy); | ||||
|                     adapter.submitList(copy); | ||||
|                 }), Dispatchers.getIO()) | ||||
|         ); | ||||
|         binding.addAccountBtn.setOnClickListener(v -> { | ||||
|             if (onAddAccountClickListener == null) return; | ||||
|             onAddAccountClickListener.onAddAccountClick(this); | ||||
|  | ||||
| @ -28,13 +28,14 @@ import java.io.File; | ||||
| 
 | ||||
| import awais.instagrabber.R; | ||||
| import awais.instagrabber.databinding.DialogProfilepicBinding; | ||||
| import awais.instagrabber.repositories.responses.User; | ||||
| import awais.instagrabber.utils.AppExecutors; | ||||
| import awais.instagrabber.utils.Constants; | ||||
| import awais.instagrabber.utils.CookieUtils; | ||||
| import awais.instagrabber.utils.CoroutineUtilsKt; | ||||
| import awais.instagrabber.utils.DownloadUtils; | ||||
| import awais.instagrabber.utils.TextUtils; | ||||
| import awais.instagrabber.webservices.ServiceCallback; | ||||
| import awais.instagrabber.webservices.UserService; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| 
 | ||||
| import static awais.instagrabber.utils.Utils.settingsHelper; | ||||
| 
 | ||||
| @ -129,33 +130,29 @@ public class ProfilePicDialogFragment extends DialogFragment { | ||||
| 
 | ||||
|     private void fetchAvatar() { | ||||
|         if (isLoggedIn) { | ||||
|             final UserService userService = UserService.getInstance(); | ||||
|             userService.getUserInfo(id, new ServiceCallback<User>() { | ||||
|                 @Override | ||||
|                 public void onSuccess(final User result) { | ||||
|                     if (result != null) { | ||||
|                         final String url = result.getHDProfilePicUrl(); | ||||
|                         if (url == null) { | ||||
|                             final Context context = getContext(); | ||||
|                             if (context == null) return; | ||||
|                             Toast.makeText(context, R.string.no_profile_pic_found, Toast.LENGTH_LONG).show(); | ||||
|                             return; | ||||
|                         } | ||||
|                         setupPhoto(url); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void onFailure(final Throwable t) { | ||||
|             final UserService userService = UserService.INSTANCE; | ||||
|             userService.getUserInfo(id, CoroutineUtilsKt.getContinuation((user, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                 if (throwable != null) { | ||||
|                     final Context context = getContext(); | ||||
|                     if (context == null) { | ||||
|                         dismiss(); | ||||
|                         return; | ||||
|                     } | ||||
|                     Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); | ||||
|                     Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show(); | ||||
|                     dismiss(); | ||||
|                     return; | ||||
|                 } | ||||
|             }); | ||||
|                 if (user != null) { | ||||
|                     final String url = user.getHDProfilePicUrl(); | ||||
|                     if (TextUtils.isEmpty(url)) { | ||||
|                         final Context context = getContext(); | ||||
|                         if (context == null) return; | ||||
|                         Toast.makeText(context, R.string.no_profile_pic_found, Toast.LENGTH_LONG).show(); | ||||
|                         return; | ||||
|                     } | ||||
|                     setupPhoto(url); | ||||
|                 } | ||||
|             }), Dispatchers.getIO())); | ||||
|         } else setupPhoto(fallbackUrl); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -30,9 +30,12 @@ import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; | ||||
| import awais.instagrabber.databinding.FragmentFollowersViewerBinding; | ||||
| import awais.instagrabber.models.FollowModel; | ||||
| import awais.instagrabber.repositories.responses.FriendshipListFetchResponse; | ||||
| import awais.instagrabber.utils.AppExecutors; | ||||
| import awais.instagrabber.utils.CoroutineUtilsKt; | ||||
| import awais.instagrabber.utils.TextUtils; | ||||
| import awais.instagrabber.webservices.FriendshipService; | ||||
| import awais.instagrabber.webservices.ServiceCallback; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| import thoughtbot.expandableadapter.ExpandableGroup; | ||||
| 
 | ||||
| public final class FollowViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { | ||||
| @ -68,10 +71,32 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh | ||||
|                 if (!isFollowersList) followModels.addAll(result.getItems()); | ||||
|                 if (result.isMoreAvailable()) { | ||||
|                     endCursor = result.getNextMaxId(); | ||||
|                     friendshipService.getList(false, profileId, endCursor, this); | ||||
|                     friendshipService.getList( | ||||
|                             false, | ||||
|                             profileId, | ||||
|                             endCursor, | ||||
|                             CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                 if (throwable != null) { | ||||
|                                     onFailure(throwable); | ||||
|                                     return; | ||||
|                                 } | ||||
|                                 onSuccess(response); | ||||
|                             }), Dispatchers.getIO()) | ||||
|                     ); | ||||
|                 } else if (followersModels.size() == 0) { | ||||
|                     if (!isFollowersList) moreAvailable = false; | ||||
|                     friendshipService.getList(true, profileId, null, followingFetchCb); | ||||
|                     friendshipService.getList( | ||||
|                             true, | ||||
|                             profileId, | ||||
|                             null, | ||||
|                             CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                 if (throwable != null) { | ||||
|                                     followingFetchCb.onFailure(throwable); | ||||
|                                     return; | ||||
|                                 } | ||||
|                                 followingFetchCb.onSuccess(response); | ||||
|                             }), Dispatchers.getIO()) | ||||
|                     ); | ||||
|                 } else { | ||||
|                     if (!isFollowersList) moreAvailable = false; | ||||
|                     showCompare(); | ||||
| @ -84,8 +109,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh | ||||
|             try { | ||||
|                 binding.swipeRefreshLayout.setRefreshing(false); | ||||
|                 Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); | ||||
|             } | ||||
|             catch(Throwable e) {} | ||||
|             } catch (Throwable ignored) {} | ||||
|             Log.e(TAG, "Error fetching list (double, following)", t); | ||||
|         } | ||||
|     }; | ||||
| @ -97,10 +121,32 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh | ||||
|                 if (isFollowersList) followModels.addAll(result.getItems()); | ||||
|                 if (result.isMoreAvailable()) { | ||||
|                     endCursor = result.getNextMaxId(); | ||||
|                     friendshipService.getList(true, profileId, endCursor, this); | ||||
|                     friendshipService.getList( | ||||
|                             true, | ||||
|                             profileId, | ||||
|                             endCursor, | ||||
|                             CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                 if (throwable != null) { | ||||
|                                     onFailure(throwable); | ||||
|                                     return; | ||||
|                                 } | ||||
|                                 onSuccess(response); | ||||
|                             }), Dispatchers.getIO()) | ||||
|                     ); | ||||
|                 } else if (followingModels.size() == 0) { | ||||
|                     if (isFollowersList) moreAvailable = false; | ||||
|                     friendshipService.getList(false, profileId, null, followingFetchCb); | ||||
|                     friendshipService.getList( | ||||
|                             false, | ||||
|                             profileId, | ||||
|                             null, | ||||
|                             CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                 if (throwable != null) { | ||||
|                                     followingFetchCb.onFailure(throwable); | ||||
|                                     return; | ||||
|                                 } | ||||
|                                 followingFetchCb.onSuccess(response); | ||||
|                             }), Dispatchers.getIO()) | ||||
|                     ); | ||||
|                 } else { | ||||
|                     if (isFollowersList) moreAvailable = false; | ||||
|                     showCompare(); | ||||
| @ -113,8 +159,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh | ||||
|             try { | ||||
|                 binding.swipeRefreshLayout.setRefreshing(false); | ||||
|                 Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); | ||||
|             } | ||||
|             catch(Throwable e) {} | ||||
|             } catch (Throwable ignored) {} | ||||
|             Log.e(TAG, "Error fetching list (double, follower)", t); | ||||
|         } | ||||
|     }; | ||||
| @ -122,7 +167,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh | ||||
|     @Override | ||||
|     public void onCreate(@Nullable final Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         friendshipService = FriendshipService.getInstance(null, null, 0); | ||||
|         friendshipService = FriendshipService.INSTANCE; | ||||
|         fragmentActivity = (AppCompatActivity) getActivity(); | ||||
|         setHasOptionsMenu(true); | ||||
|     } | ||||
| @ -235,8 +280,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh | ||||
|                 try { | ||||
|                     binding.swipeRefreshLayout.setRefreshing(false); | ||||
|                     Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); | ||||
|                 } | ||||
|                 catch(Throwable e) {} | ||||
|                 } catch (Throwable ignored) {} | ||||
|                 Log.e(TAG, "Error fetching list (single)", t); | ||||
|             } | ||||
|         }; | ||||
| @ -245,7 +289,18 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh | ||||
|             if (!TextUtils.isEmpty(endCursor) && !searching) { | ||||
|                 binding.swipeRefreshLayout.setRefreshing(true); | ||||
|                 layoutManager.setStackFromEnd(true); | ||||
|                 friendshipService.getList(isFollowersList, profileId, endCursor, cb); | ||||
|                 friendshipService.getList( | ||||
|                         isFollowersList, | ||||
|                         profileId, | ||||
|                         endCursor, | ||||
|                         CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                             if (throwable != null) { | ||||
|                                 cb.onFailure(throwable); | ||||
|                                 return; | ||||
|                             } | ||||
|                             cb.onSuccess(response); | ||||
|                         }), Dispatchers.getIO()) | ||||
|                 ); | ||||
|                 endCursor = null; | ||||
|             } | ||||
|         }); | ||||
| @ -253,7 +308,18 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh | ||||
|         binding.rvFollow.setLayoutManager(layoutManager); | ||||
|         if (moreAvailable) { | ||||
|             binding.swipeRefreshLayout.setRefreshing(true); | ||||
|             friendshipService.getList(isFollowersList, profileId, endCursor, cb); | ||||
|             friendshipService.getList( | ||||
|                     isFollowersList, | ||||
|                     profileId, | ||||
|                     endCursor, | ||||
|                     CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                         if (throwable != null) { | ||||
|                             cb.onFailure(throwable); | ||||
|                             return; | ||||
|                         } | ||||
|                         cb.onSuccess(response); | ||||
|                     }), Dispatchers.getIO()) | ||||
|             ); | ||||
|         } else { | ||||
|             refreshAdapter(followModels, null, null, null); | ||||
|             layoutManager.scrollToPosition(0); | ||||
| @ -269,17 +335,34 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh | ||||
|         if (moreAvailable) { | ||||
|             binding.swipeRefreshLayout.setRefreshing(true); | ||||
|             Toast.makeText(getContext(), R.string.follower_start_compare, Toast.LENGTH_LONG).show(); | ||||
|             friendshipService.getList(isFollowersList, | ||||
|                                       profileId, | ||||
|                                       endCursor, | ||||
|                                       isFollowersList ? followersFetchCb : followingFetchCb); | ||||
|             friendshipService.getList( | ||||
|                     isFollowersList, | ||||
|                     profileId, | ||||
|                     endCursor, | ||||
|                     CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                         final ServiceCallback<FriendshipListFetchResponse> callback = isFollowersList ? followersFetchCb : followingFetchCb; | ||||
|                         if (throwable != null) { | ||||
|                             callback.onFailure(throwable); | ||||
|                             return; | ||||
|                         } | ||||
|                         callback.onSuccess(response); | ||||
|                     }), Dispatchers.getIO()) | ||||
|             ); | ||||
|         } else if (followersModels.size() == 0 || followingModels.size() == 0) { | ||||
|             binding.swipeRefreshLayout.setRefreshing(true); | ||||
|             Toast.makeText(getContext(), R.string.follower_start_compare, Toast.LENGTH_LONG).show(); | ||||
|             friendshipService.getList(!isFollowersList, | ||||
|                                       profileId, | ||||
|                                       null, | ||||
|                                       isFollowersList ? followingFetchCb : followersFetchCb); | ||||
|             friendshipService.getList( | ||||
|                     !isFollowersList, | ||||
|                     profileId, | ||||
|                     null, | ||||
|                     CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                         final ServiceCallback<FriendshipListFetchResponse> callback = isFollowersList ? followingFetchCb : followersFetchCb; | ||||
|                         if (throwable != null) { | ||||
|                             callback.onFailure(throwable); | ||||
|                             return; | ||||
|                         } | ||||
|                         callback.onSuccess(response); | ||||
|                     }), Dispatchers.getIO())); | ||||
|         } else showCompare(); | ||||
|     } | ||||
| 
 | ||||
| @ -337,10 +420,10 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh | ||||
|         final Context context = getContext(); | ||||
|         if (loading) Toast.makeText(context, R.string.follower_wait_to_load, Toast.LENGTH_LONG).show(); | ||||
|         else if (isCompare) { | ||||
|             isCompare = !isCompare; | ||||
|             isCompare = false; | ||||
|             listFollows(); | ||||
|         } else { | ||||
|             isCompare = !isCompare; | ||||
|             isCompare = true; | ||||
|             listCompare(); | ||||
|         } | ||||
|         return true; | ||||
| @ -354,16 +437,15 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh | ||||
|         final ArrayList<ExpandableGroup> groups = new ArrayList<>(1); | ||||
| 
 | ||||
|         if (isCompare && followingModels != null && followersModels != null && allFollowing != null) { | ||||
|             if (followingModels != null && followingModels.size() > 0) | ||||
|             if (followingModels.size() > 0) | ||||
|                 groups.add(new ExpandableGroup(resources.getString(R.string.followers_not_following, username), followingModels)); | ||||
|             if (followersModels != null && followersModels.size() > 0) | ||||
|             if (followersModels.size() > 0) | ||||
|                 groups.add(new ExpandableGroup(resources.getString(R.string.followers_not_follower, namePost), followersModels)); | ||||
|             if (allFollowing != null && allFollowing.size() > 0) | ||||
|             if (allFollowing.size() > 0) | ||||
|                 groups.add(new ExpandableGroup(resources.getString(R.string.followers_both_following), allFollowing)); | ||||
|         } else if (followModels != null) { | ||||
|             groups.add(new ExpandableGroup(type, followModels)); | ||||
|         } | ||||
|         else return; | ||||
|         } else return; | ||||
|         adapter = new FollowAdapter(clickListener, groups); | ||||
|         adapter.toggleGroup(0); | ||||
|         binding.rvFollow.setAdapter(adapter); | ||||
|  | ||||
| @ -39,7 +39,6 @@ import com.google.android.material.snackbar.Snackbar; | ||||
| import com.google.common.collect.ImmutableList; | ||||
| 
 | ||||
| import java.time.LocalDateTime; | ||||
| import java.util.List; | ||||
| import java.util.Set; | ||||
| 
 | ||||
| import awais.instagrabber.R; | ||||
| @ -55,7 +54,6 @@ import awais.instagrabber.db.repositories.FavoriteRepository; | ||||
| import awais.instagrabber.db.repositories.RepositoryCallback; | ||||
| import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; | ||||
| import awais.instagrabber.models.PostsLayoutPreferences; | ||||
| import awais.instagrabber.models.StoryModel; | ||||
| import awais.instagrabber.models.enums.FavoriteType; | ||||
| import awais.instagrabber.models.enums.FollowingType; | ||||
| import awais.instagrabber.repositories.requests.StoryViewerOptions; | ||||
| @ -63,8 +61,10 @@ import awais.instagrabber.repositories.responses.Hashtag; | ||||
| import awais.instagrabber.repositories.responses.Location; | ||||
| import awais.instagrabber.repositories.responses.Media; | ||||
| import awais.instagrabber.repositories.responses.User; | ||||
| import awais.instagrabber.utils.AppExecutors; | ||||
| import awais.instagrabber.utils.Constants; | ||||
| import awais.instagrabber.utils.CookieUtils; | ||||
| import awais.instagrabber.utils.CoroutineUtilsKt; | ||||
| import awais.instagrabber.utils.DownloadUtils; | ||||
| import awais.instagrabber.utils.TextUtils; | ||||
| import awais.instagrabber.utils.Utils; | ||||
| @ -72,6 +72,7 @@ import awais.instagrabber.webservices.GraphQLService; | ||||
| import awais.instagrabber.webservices.ServiceCallback; | ||||
| import awais.instagrabber.webservices.StoriesService; | ||||
| import awais.instagrabber.webservices.TagsService; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| 
 | ||||
| import static androidx.core.content.PermissionChecker.checkSelfPermission; | ||||
| import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; | ||||
| @ -218,20 +219,15 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|             if (TextUtils.isEmpty(user.getUsername())) { | ||||
|                 // this only happens for anons | ||||
|                 opening = true; | ||||
|                 graphQLService.fetchPost(feedModel.getCode(), new ServiceCallback<Media>() { | ||||
|                     @Override | ||||
|                     public void onSuccess(final Media newFeedModel) { | ||||
|                         opening = false; | ||||
|                         if (newFeedModel == null) return; | ||||
|                         openPostDialog(newFeedModel, profilePicView, mainPostImage, position); | ||||
|                 graphQLService.fetchPost(feedModel.getCode(), CoroutineUtilsKt.getContinuation((media, throwable) -> { | ||||
|                     opening = false; | ||||
|                     if (throwable != null) { | ||||
|                         Log.e(TAG, "Error", throwable); | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     @Override | ||||
|                     public void onFailure(final Throwable t) { | ||||
|                         opening = false; | ||||
|                         Log.e(TAG, "Error", t); | ||||
|                     } | ||||
|                 }); | ||||
|                     if (media == null) return; | ||||
|                     AppExecutors.INSTANCE.getMainThread().execute(() -> openPostDialog(media, profilePicView, mainPostImage, position)); | ||||
|                 }, Dispatchers.getIO())); | ||||
|                 return; | ||||
|             } | ||||
|             opening = true; | ||||
| @ -303,8 +299,8 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|         final String cookie = settingsHelper.getString(Constants.COOKIE); | ||||
|         isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; | ||||
|         tagsService = isLoggedIn ? TagsService.getInstance() : null; | ||||
|         storiesService = isLoggedIn ? StoriesService.getInstance(null, 0L, null) : null; | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); | ||||
|         storiesService = isLoggedIn ? StoriesService.INSTANCE : null; | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; | ||||
|         setHasOptionsMenu(true); | ||||
|     } | ||||
| 
 | ||||
| @ -385,7 +381,13 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|     private void fetchHashtagModel() { | ||||
|         binding.swipeRefreshLayout.setRefreshing(true); | ||||
|         if (isLoggedIn) tagsService.fetch(hashtag, cb); | ||||
|         else graphQLService.fetchTag(hashtag, cb); | ||||
|         else graphQLService.fetchTag(hashtag, CoroutineUtilsKt.getContinuation((hashtag1, throwable) -> { | ||||
|             if (throwable != null) { | ||||
|                 cb.onFailure(throwable); | ||||
|                 return; | ||||
|             } | ||||
|             AppExecutors.INSTANCE.getMainThread().execute(() -> cb.onSuccess(hashtag1)); | ||||
|         }, Dispatchers.getIO())); | ||||
|     } | ||||
| 
 | ||||
|     private void setupPosts() { | ||||
| @ -578,24 +580,21 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|         storiesFetching = true; | ||||
|         storiesService.getUserStory( | ||||
|                 StoryViewerOptions.forHashtag(hashtagModel.getName()), | ||||
|                 new ServiceCallback<List<StoryModel>>() { | ||||
|                     @Override | ||||
|                     public void onSuccess(final List<StoryModel> storyModels) { | ||||
|                         if (storyModels != null && !storyModels.isEmpty()) { | ||||
|                             hashtagDetailsBinding.mainHashtagImage.setStoriesBorder(1); | ||||
|                             hasStories = true; | ||||
|                         } else { | ||||
|                             hasStories = false; | ||||
|                         } | ||||
|                 CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                     if (throwable != null) { | ||||
|                         Log.e(TAG, "Error", throwable); | ||||
|                         storiesFetching = false; | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     @Override | ||||
|                     public void onFailure(final Throwable t) { | ||||
|                         Log.e(TAG, "Error", t); | ||||
|                         storiesFetching = false; | ||||
|                     if (storyModels != null && !storyModels.isEmpty()) { | ||||
|                         hashtagDetailsBinding.mainHashtagImage.setStoriesBorder(1); | ||||
|                         hasStories = true; | ||||
|                     } else { | ||||
|                         hasStories = false; | ||||
|                     } | ||||
|                 }); | ||||
|                     storiesFetching = false; | ||||
|                 }), Dispatchers.getIO()) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private void setTitle() { | ||||
|  | ||||
| @ -109,11 +109,11 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme | ||||
|         final String cookie = settingsHelper.getString(Constants.COOKIE); | ||||
|         final long userId = CookieUtils.getUserIdFromCookie(cookie); | ||||
|         isLoggedIn = !TextUtils.isEmpty(cookie) && userId != 0; | ||||
|         final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); | ||||
|         // final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); | ||||
|         final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); | ||||
|         if (csrfToken == null) return; | ||||
|         mediaService = isLoggedIn ? MediaService.getInstance(deviceUuid, csrfToken, userId) : null; | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); | ||||
|         mediaService = isLoggedIn ? MediaService.INSTANCE : null; | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; | ||||
|         // setHasOptionsMenu(true); | ||||
|     } | ||||
| 
 | ||||
| @ -135,7 +135,17 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme | ||||
|     public void onRefresh() { | ||||
|         if (isComment && !isLoggedIn) { | ||||
|             lazyLoader.resetState(); | ||||
|             graphQLService.fetchCommentLikers(postId, null, anonCb); | ||||
|             graphQLService.fetchCommentLikers( | ||||
|                     postId, | ||||
|                     null, | ||||
|                     CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                         if (throwable != null) { | ||||
|                             anonCb.onFailure(throwable); | ||||
|                             return; | ||||
|                         } | ||||
|                         anonCb.onSuccess(response); | ||||
|                     }), Dispatchers.getIO()) | ||||
|             ); | ||||
|         } else { | ||||
|             mediaService.fetchLikes( | ||||
|                     postId, | ||||
| @ -164,8 +174,19 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme | ||||
|             binding.rvLikes.setLayoutManager(layoutManager); | ||||
|             binding.rvLikes.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.HORIZONTAL)); | ||||
|             lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { | ||||
|                 if (!TextUtils.isEmpty(endCursor)) | ||||
|                     graphQLService.fetchCommentLikers(postId, endCursor, anonCb); | ||||
|                 if (!TextUtils.isEmpty(endCursor)) { | ||||
|                     graphQLService.fetchCommentLikers( | ||||
|                             postId, | ||||
|                             endCursor, | ||||
|                             CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                 if (throwable != null) { | ||||
|                                     anonCb.onFailure(throwable); | ||||
|                                     return; | ||||
|                                 } | ||||
|                                 anonCb.onSuccess(response); | ||||
|                             }), Dispatchers.getIO()) | ||||
|                     ); | ||||
|                 } | ||||
|                 endCursor = null; | ||||
|             }); | ||||
|             binding.rvLikes.addOnScrollListener(lazyLoader); | ||||
|  | ||||
| @ -37,7 +37,6 @@ import com.google.android.material.snackbar.Snackbar; | ||||
| import com.google.common.collect.ImmutableList; | ||||
| 
 | ||||
| import java.time.LocalDateTime; | ||||
| import java.util.List; | ||||
| import java.util.Set; | ||||
| 
 | ||||
| import awais.instagrabber.R; | ||||
| @ -53,14 +52,15 @@ import awais.instagrabber.db.repositories.FavoriteRepository; | ||||
| import awais.instagrabber.db.repositories.RepositoryCallback; | ||||
| import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; | ||||
| import awais.instagrabber.models.PostsLayoutPreferences; | ||||
| import awais.instagrabber.models.StoryModel; | ||||
| import awais.instagrabber.models.enums.FavoriteType; | ||||
| import awais.instagrabber.repositories.requests.StoryViewerOptions; | ||||
| import awais.instagrabber.repositories.responses.Location; | ||||
| import awais.instagrabber.repositories.responses.Media; | ||||
| import awais.instagrabber.repositories.responses.User; | ||||
| import awais.instagrabber.utils.AppExecutors; | ||||
| import awais.instagrabber.utils.Constants; | ||||
| import awais.instagrabber.utils.CookieUtils; | ||||
| import awais.instagrabber.utils.CoroutineUtilsKt; | ||||
| import awais.instagrabber.utils.DownloadUtils; | ||||
| import awais.instagrabber.utils.TextUtils; | ||||
| import awais.instagrabber.utils.Utils; | ||||
| @ -68,6 +68,7 @@ import awais.instagrabber.webservices.GraphQLService; | ||||
| import awais.instagrabber.webservices.LocationService; | ||||
| import awais.instagrabber.webservices.ServiceCallback; | ||||
| import awais.instagrabber.webservices.StoriesService; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| 
 | ||||
| import static androidx.core.content.PermissionChecker.checkSelfPermission; | ||||
| import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; | ||||
| @ -208,20 +209,18 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR | ||||
|             if (user == null) return; | ||||
|             if (TextUtils.isEmpty(user.getUsername())) { | ||||
|                 opening = true; | ||||
|                 graphQLService.fetchPost(feedModel.getCode(), new ServiceCallback<Media>() { | ||||
|                     @Override | ||||
|                     public void onSuccess(final Media newFeedModel) { | ||||
|                         opening = false; | ||||
|                         if (newFeedModel == null) return; | ||||
|                         openPostDialog(newFeedModel, profilePicView, mainPostImage, position); | ||||
|                     } | ||||
| 
 | ||||
|                     @Override | ||||
|                     public void onFailure(final Throwable t) { | ||||
|                         opening = false; | ||||
|                         Log.e(TAG, "Error", t); | ||||
|                     } | ||||
|                 }); | ||||
|                 graphQLService.fetchPost( | ||||
|                         feedModel.getCode(), | ||||
|                         CoroutineUtilsKt.getContinuation((media, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                             opening = false; | ||||
|                             if (throwable != null) { | ||||
|                                 Log.e(TAG, "Error", throwable); | ||||
|                                 return; | ||||
|                             } | ||||
|                             if (media == null) return; | ||||
|                             openPostDialog(media, profilePicView, mainPostImage, position); | ||||
|                         })) | ||||
|                 ); | ||||
|                 return; | ||||
|             } | ||||
|             opening = true; | ||||
| @ -293,8 +292,8 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR | ||||
|         final String cookie = settingsHelper.getString(Constants.COOKIE); | ||||
|         isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; | ||||
|         locationService = isLoggedIn ? LocationService.getInstance() : null; | ||||
|         storiesService = StoriesService.getInstance(null, 0L, null); | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); | ||||
|         storiesService = StoriesService.INSTANCE; | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; | ||||
|         setHasOptionsMenu(true); | ||||
|     } | ||||
| 
 | ||||
| @ -402,7 +401,16 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR | ||||
|     private void fetchLocationModel() { | ||||
|         binding.swipeRefreshLayout.setRefreshing(true); | ||||
|         if (isLoggedIn) locationService.fetch(locationId, cb); | ||||
|         else graphQLService.fetchLocation(locationId, cb); | ||||
|         else graphQLService.fetchLocation( | ||||
|                 locationId, | ||||
|                 CoroutineUtilsKt.getContinuation((location, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                     if (throwable != null) { | ||||
|                         cb.onFailure(throwable); | ||||
|                         return; | ||||
|                     } | ||||
|                     cb.onSuccess(location); | ||||
|                 })) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private void setupLocationDetails() { | ||||
| @ -577,22 +585,19 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR | ||||
|             storiesFetching = true; | ||||
|             storiesService.getUserStory( | ||||
|                     StoryViewerOptions.forLocation(locationId, locationModel.getName()), | ||||
|                     new ServiceCallback<List<StoryModel>>() { | ||||
|                         @Override | ||||
|                         public void onSuccess(final List<StoryModel> storyModels) { | ||||
|                             if (storyModels != null && !storyModels.isEmpty()) { | ||||
|                                 locationDetailsBinding.mainLocationImage.setStoriesBorder(1); | ||||
|                                 hasStories = true; | ||||
|                             } | ||||
|                     CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                         if (throwable != null) { | ||||
|                             Log.e(TAG, "Error", throwable); | ||||
|                             storiesFetching = false; | ||||
|                             return; | ||||
|                         } | ||||
| 
 | ||||
|                         @Override | ||||
|                         public void onFailure(final Throwable t) { | ||||
|                             Log.e(TAG, "Error", t); | ||||
|                             storiesFetching = false; | ||||
|                         if (storyModels != null && !storyModels.isEmpty()) { | ||||
|                             locationDetailsBinding.mainLocationImage.setStoriesBorder(1); | ||||
|                             hasStories = true; | ||||
|                         } | ||||
|                     }); | ||||
|                         storiesFetching = false; | ||||
|                     }), Dispatchers.getIO()) | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -34,7 +34,6 @@ import awais.instagrabber.adapters.NotificationsAdapter.OnNotificationClickListe | ||||
| import awais.instagrabber.databinding.FragmentNotificationsViewerBinding; | ||||
| import awais.instagrabber.models.enums.NotificationType; | ||||
| import awais.instagrabber.repositories.requests.StoryViewerOptions; | ||||
| import awais.instagrabber.repositories.responses.FriendshipChangeResponse; | ||||
| import awais.instagrabber.repositories.responses.notification.Notification; | ||||
| import awais.instagrabber.repositories.responses.notification.NotificationArgs; | ||||
| import awais.instagrabber.repositories.responses.notification.NotificationImage; | ||||
| @ -68,6 +67,7 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe | ||||
|     private String type; | ||||
|     private long targetId; | ||||
|     private Context context; | ||||
|     private long userId; | ||||
| 
 | ||||
|     private final ServiceCallback<List<Notification>> cb = new ServiceCallback<List<Notification>>() { | ||||
|         @Override | ||||
| @ -168,34 +168,40 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe | ||||
|                             break; | ||||
|                         case 1: | ||||
|                             if (model.getType() == NotificationType.REQUEST) { | ||||
|                                 friendshipService.approve(args.getUserId(), new ServiceCallback<FriendshipChangeResponse>() { | ||||
|                                     @Override | ||||
|                                     public void onSuccess(final FriendshipChangeResponse result) { | ||||
|                                         onRefresh(); | ||||
|                                         Log.e(TAG, "approve: status was not ok!"); | ||||
|                                     } | ||||
| 
 | ||||
|                                     @Override | ||||
|                                     public void onFailure(final Throwable t) { | ||||
|                                         Log.e(TAG, "approve: onFailure: ", t); | ||||
|                                     } | ||||
|                                 }); | ||||
|                                 friendshipService.approve( | ||||
|                                         csrfToken, | ||||
|                                         userId, | ||||
|                                         deviceUuid, | ||||
|                                         args.getUserId(), | ||||
|                                         CoroutineUtilsKt.getContinuation( | ||||
|                                                 (response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                                     if (throwable != null) { | ||||
|                                                         Log.e(TAG, "approve: onFailure: ", throwable); | ||||
|                                                         return; | ||||
|                                                     } | ||||
|                                                     onRefresh(); | ||||
|                                                 }), | ||||
|                                                 Dispatchers.getIO() | ||||
|                                         ) | ||||
|                                 ); | ||||
|                                 return; | ||||
|                             } | ||||
|                             clickListener.onPreviewClick(model); | ||||
|                             break; | ||||
|                         case 2: | ||||
|                             friendshipService.ignore(args.getUserId(), new ServiceCallback<FriendshipChangeResponse>() { | ||||
|                                 @Override | ||||
|                                 public void onSuccess(final FriendshipChangeResponse result) { | ||||
|                                     onRefresh(); | ||||
|                                 } | ||||
| 
 | ||||
|                                 @Override | ||||
|                                 public void onFailure(final Throwable t) { | ||||
|                                     Log.e(TAG, "ignore: onFailure: ", t); | ||||
|                                 } | ||||
|                             }); | ||||
|                             friendshipService.ignore( | ||||
|                                     csrfToken, | ||||
|                                     userId, | ||||
|                                     deviceUuid, | ||||
|                                     args.getUserId(), | ||||
|                                     CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                         if (throwable != null) { | ||||
|                                             Log.e(TAG, "approve: onFailure: ", throwable); | ||||
|                                             return; | ||||
|                                         } | ||||
|                                         onRefresh(); | ||||
|                                     }), Dispatchers.getIO()) | ||||
|                             ); | ||||
|                             break; | ||||
|                     } | ||||
|                 }; | ||||
| @ -219,11 +225,11 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe | ||||
|         if (TextUtils.isEmpty(cookie)) { | ||||
|             Toast.makeText(context, R.string.activity_notloggedin, Toast.LENGTH_SHORT).show(); | ||||
|         } | ||||
|         final long userId = CookieUtils.getUserIdFromCookie(cookie); | ||||
|         userId = CookieUtils.getUserIdFromCookie(cookie); | ||||
|         deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); | ||||
|         csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); | ||||
|         friendshipService = FriendshipService.getInstance(deviceUuid, csrfToken, userId); | ||||
|         mediaService = MediaService.getInstance(deviceUuid, csrfToken, userId); | ||||
|         friendshipService = FriendshipService.INSTANCE; | ||||
|         mediaService = MediaService.INSTANCE; | ||||
|         newsService = NewsService.getInstance(); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -41,12 +41,15 @@ import awais.instagrabber.fragments.settings.MorePreferencesFragmentDirections; | ||||
| import awais.instagrabber.models.FeedStoryModel; | ||||
| import awais.instagrabber.models.HighlightModel; | ||||
| import awais.instagrabber.repositories.requests.StoryViewerOptions; | ||||
| import awais.instagrabber.utils.AppExecutors; | ||||
| import awais.instagrabber.utils.CoroutineUtilsKt; | ||||
| import awais.instagrabber.utils.TextUtils; | ||||
| import awais.instagrabber.viewmodels.ArchivesViewModel; | ||||
| import awais.instagrabber.viewmodels.FeedStoriesViewModel; | ||||
| import awais.instagrabber.webservices.ServiceCallback; | ||||
| import awais.instagrabber.webservices.StoriesService; | ||||
| import awais.instagrabber.webservices.StoriesService.ArchiveFetchResponse; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| 
 | ||||
| public final class StoryListViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { | ||||
|     private static final String TAG = "StoryListViewerFragment"; | ||||
| @ -133,7 +136,7 @@ public final class StoryListViewerFragment extends Fragment implements SwipeRefr | ||||
|         context = getContext(); | ||||
|         if (context == null) return; | ||||
|         setHasOptionsMenu(true); | ||||
|         storiesService = StoriesService.getInstance(null, 0L, null); | ||||
|         storiesService = StoriesService.INSTANCE; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
| @ -239,22 +242,31 @@ public final class StoryListViewerFragment extends Fragment implements SwipeRefr | ||||
|             } | ||||
|             firstRefresh = false; | ||||
|         } else if (type.equals("feed")) { | ||||
|             storiesService.getFeedStories(new ServiceCallback<List<FeedStoryModel>>() { | ||||
|                 @Override | ||||
|                 public void onSuccess(final List<FeedStoryModel> result) { | ||||
|                     feedStoriesViewModel.getList().postValue(result); | ||||
|                     adapter.submitList(result); | ||||
|                     binding.swipeRefreshLayout.setRefreshing(false); | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void onFailure(final Throwable t) { | ||||
|                     Log.e(TAG, "failed", t); | ||||
|                     Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); | ||||
|                 } | ||||
|             }); | ||||
|             storiesService.getFeedStories( | ||||
|                     CoroutineUtilsKt.getContinuation((feedStoryModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                         if (throwable != null) { | ||||
|                             Log.e(TAG, "failed", throwable); | ||||
|                             Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show(); | ||||
|                             return; | ||||
|                         } | ||||
|                         //noinspection unchecked | ||||
|                         feedStoriesViewModel.getList().postValue((List<FeedStoryModel>) feedStoryModels); | ||||
|                         //noinspection unchecked | ||||
|                         adapter.submitList((List<FeedStoryModel>) feedStoryModels); | ||||
|                         binding.swipeRefreshLayout.setRefreshing(false); | ||||
|                     }), Dispatchers.getIO()) | ||||
|             ); | ||||
|         } else if (type.equals("archive")) { | ||||
|             storiesService.fetchArchive(endCursor, cb); | ||||
|             storiesService.fetchArchive( | ||||
|                     endCursor, | ||||
|                     CoroutineUtilsKt.getContinuation((archiveFetchResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                         if (throwable != null) { | ||||
|                             cb.onFailure(throwable); | ||||
|                             return; | ||||
|                         } | ||||
|                         cb.onSuccess(archiveFetchResponse); | ||||
|                     }), Dispatchers.getIO()) | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -87,7 +87,6 @@ import awais.instagrabber.models.stickers.SwipeUpModel; | ||||
| import awais.instagrabber.repositories.requests.StoryViewerOptions; | ||||
| import awais.instagrabber.repositories.requests.StoryViewerOptions.Type; | ||||
| import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds; | ||||
| import awais.instagrabber.repositories.responses.StoryStickerResponse; | ||||
| import awais.instagrabber.utils.AppExecutors; | ||||
| import awais.instagrabber.utils.Constants; | ||||
| import awais.instagrabber.utils.CookieUtils; | ||||
| @ -113,6 +112,8 @@ import static awais.instagrabber.utils.Utils.settingsHelper; | ||||
| public class StoryViewerFragment extends Fragment { | ||||
|     private static final String TAG = "StoryViewerFragment"; | ||||
| 
 | ||||
|     private final String cookie = settingsHelper.getString(Constants.COOKIE); | ||||
| 
 | ||||
|     private AppCompatActivity fragmentActivity; | ||||
|     private View root; | ||||
|     private FragmentStoryViewerBinding binding; | ||||
| @ -148,21 +149,22 @@ public class StoryViewerFragment extends Fragment { | ||||
|     // private boolean isArchive; | ||||
|     // private boolean isNotification; | ||||
|     private DirectMessagesService directMessagesService; | ||||
| 
 | ||||
|     private final String cookie = settingsHelper.getString(Constants.COOKIE); | ||||
|     private StoryViewerOptions options; | ||||
|     private String csrfToken; | ||||
|     private String deviceId; | ||||
|     private long userId; | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate(@Nullable final Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); | ||||
|         csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); | ||||
|         if (csrfToken == null) return; | ||||
|         final long userIdFromCookie = CookieUtils.getUserIdFromCookie(cookie); | ||||
|         final String deviceId = settingsHelper.getString(Constants.DEVICE_UUID); | ||||
|         userId = CookieUtils.getUserIdFromCookie(cookie); | ||||
|         deviceId = settingsHelper.getString(Constants.DEVICE_UUID); | ||||
|         fragmentActivity = (AppCompatActivity) requireActivity(); | ||||
|         storiesService = StoriesService.getInstance(csrfToken, userIdFromCookie, deviceId); | ||||
|         mediaService = MediaService.getInstance(deviceId, csrfToken, userIdFromCookie); | ||||
|         directMessagesService = DirectMessagesService.getInstance(csrfToken, userIdFromCookie, deviceId); | ||||
|         storiesService = StoriesService.INSTANCE; | ||||
|         mediaService = MediaService.INSTANCE; | ||||
|         directMessagesService = DirectMessagesService.INSTANCE; | ||||
|         setHasOptionsMenu(true); | ||||
|     } | ||||
| 
 | ||||
| @ -220,6 +222,9 @@ public class StoryViewerFragment extends Fragment { | ||||
|                     .setTitle(R.string.reply_story) | ||||
|                     .setView(input) | ||||
|                     .setPositiveButton(R.string.confirm, (d, w) -> directMessagesService.createThread( | ||||
|                             csrfToken, | ||||
|                             userId, | ||||
|                             deviceId, | ||||
|                             Collections.singletonList(currentStory.getUserId()), | ||||
|                             null, | ||||
|                             CoroutineUtilsKt.getContinuation((thread, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
| @ -229,6 +234,9 @@ public class StoryViewerFragment extends Fragment { | ||||
|                                     return; | ||||
|                                 } | ||||
|                                 directMessagesService.broadcastStoryReply( | ||||
|                                         csrfToken, | ||||
|                                         userId, | ||||
|                                         deviceId, | ||||
|                                         ThreadIdOrUserIds.of(thread.getThreadId()), | ||||
|                                         input.getText().toString(), | ||||
|                                         currentStory.getStoryMediaId(), | ||||
| @ -514,28 +522,31 @@ public class StoryViewerFragment extends Fragment { | ||||
|                             }), (d, w) -> { | ||||
|                                 sticking = true; | ||||
|                                 storiesService.respondToPoll( | ||||
|                                         csrfToken, | ||||
|                                         userId, | ||||
|                                         deviceId, | ||||
|                                         currentStory.getStoryMediaId().split("_")[0], | ||||
|                                         poll.getId(), | ||||
|                                         w, | ||||
|                                         new ServiceCallback<StoryStickerResponse>() { | ||||
|                                             @Override | ||||
|                                             public void onSuccess(final StoryStickerResponse result) { | ||||
|                                                 sticking = false; | ||||
|                                                 try { | ||||
|                                                     poll.setMyChoice(w); | ||||
|                                                     Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show(); | ||||
|                                                 } catch (Exception ignored) {} | ||||
|                                             } | ||||
| 
 | ||||
|                                             @Override | ||||
|                                             public void onFailure(final Throwable t) { | ||||
|                                                 sticking = false; | ||||
|                                                 Log.e(TAG, "Error responding", t); | ||||
|                                                 try { | ||||
|                                                     Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); | ||||
|                                                 } catch (Exception ignored) {} | ||||
|                                             } | ||||
|                                         }); | ||||
|                                         CoroutineUtilsKt.getContinuation( | ||||
|                                                 (storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                                     if (throwable != null) { | ||||
|                                                         sticking = false; | ||||
|                                                         Log.e(TAG, "Error responding", throwable); | ||||
|                                                         try { | ||||
|                                                             Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); | ||||
|                                                         } catch (Exception ignored) {} | ||||
|                                                         return; | ||||
|                                                     } | ||||
|                                                     sticking = false; | ||||
|                                                     try { | ||||
|                                                         poll.setMyChoice(w); | ||||
|                                                         Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show(); | ||||
|                                                     } catch (Exception ignored) {} | ||||
|                                                 }), | ||||
|                                                 Dispatchers.getIO() | ||||
|                                         ) | ||||
|                                 ); | ||||
|                             }) | ||||
|                             .setPositiveButton(R.string.cancel, null) | ||||
|                             .show(); | ||||
| @ -550,27 +561,30 @@ public class StoryViewerFragment extends Fragment { | ||||
|                         .setPositiveButton(R.string.confirm, (d, w) -> { | ||||
|                             sticking = true; | ||||
|                             storiesService.respondToQuestion( | ||||
|                                     csrfToken, | ||||
|                                     userId, | ||||
|                                     deviceId, | ||||
|                                     currentStory.getStoryMediaId().split("_")[0], | ||||
|                                     question.getId(), | ||||
|                                     input.getText().toString(), | ||||
|                                     new ServiceCallback<StoryStickerResponse>() { | ||||
|                                         @Override | ||||
|                                         public void onSuccess(final StoryStickerResponse result) { | ||||
|                                             sticking = false; | ||||
|                                             try { | ||||
|                                                 Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); | ||||
|                                             } catch (Exception ignored) {} | ||||
|                                         } | ||||
| 
 | ||||
|                                         @Override | ||||
|                                         public void onFailure(final Throwable t) { | ||||
|                                             sticking = false; | ||||
|                                             Log.e(TAG, "Error responding", t); | ||||
|                                             try { | ||||
|                                                 Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); | ||||
|                                             } catch (Exception ignored) {} | ||||
|                                         } | ||||
|                                     }); | ||||
|                                     CoroutineUtilsKt.getContinuation( | ||||
|                                             (storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                                 if (throwable != null) { | ||||
|                                                     sticking = false; | ||||
|                                                     Log.e(TAG, "Error responding", throwable); | ||||
|                                                     try { | ||||
|                                                         Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); | ||||
|                                                     } catch (Exception ignored) {} | ||||
|                                                     return; | ||||
|                                                 } | ||||
|                                                 sticking = false; | ||||
|                                                 try { | ||||
|                                                     Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); | ||||
|                                                 } catch (Exception ignored) {} | ||||
|                                             }), | ||||
|                                             Dispatchers.getIO() | ||||
|                                     ) | ||||
|                             ); | ||||
|                         }) | ||||
|                         .setNegativeButton(R.string.cancel, null) | ||||
|                         .show(); | ||||
| @ -605,28 +619,31 @@ public class StoryViewerFragment extends Fragment { | ||||
|                             if (quiz.getMyChoice() == -1) { | ||||
|                                 sticking = true; | ||||
|                                 storiesService.respondToQuiz( | ||||
|                                         csrfToken, | ||||
|                                         userId, | ||||
|                                         deviceId, | ||||
|                                         currentStory.getStoryMediaId().split("_")[0], | ||||
|                                         quiz.getId(), | ||||
|                                         w, | ||||
|                                         new ServiceCallback<StoryStickerResponse>() { | ||||
|                                             @Override | ||||
|                                             public void onSuccess(final StoryStickerResponse result) { | ||||
|                                                 sticking = false; | ||||
|                                                 try { | ||||
|                                                     quiz.setMyChoice(w); | ||||
|                                                     Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); | ||||
|                                                 } catch (Exception ignored) {} | ||||
|                                             } | ||||
| 
 | ||||
|                                             @Override | ||||
|                                             public void onFailure(final Throwable t) { | ||||
|                                                 sticking = false; | ||||
|                                                 Log.e(TAG, "Error responding", t); | ||||
|                                                 try { | ||||
|                                                     Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); | ||||
|                                                 } catch (Exception ignored) {} | ||||
|                                             } | ||||
|                                         }); | ||||
|                                         CoroutineUtilsKt.getContinuation( | ||||
|                                                 (storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                                     if (throwable != null) { | ||||
|                                                         sticking = false; | ||||
|                                                         Log.e(TAG, "Error responding", throwable); | ||||
|                                                         try { | ||||
|                                                             Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); | ||||
|                                                         } catch (Exception ignored) {} | ||||
|                                                         return; | ||||
|                                                     } | ||||
|                                                     sticking = false; | ||||
|                                                     try { | ||||
|                                                         quiz.setMyChoice(w); | ||||
|                                                         Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); | ||||
|                                                     } catch (Exception ignored) {} | ||||
|                                                 }), | ||||
|                                                 Dispatchers.getIO() | ||||
|                                         ) | ||||
|                                 ); | ||||
|                             } | ||||
|                         }) | ||||
|                         .setPositiveButton(R.string.cancel, null) | ||||
| @ -673,28 +690,30 @@ public class StoryViewerFragment extends Fragment { | ||||
|                             .setPositiveButton(R.string.confirm, (d, w) -> { | ||||
|                                 sticking = true; | ||||
|                                 storiesService.respondToSlider( | ||||
|                                         csrfToken, | ||||
|                                         userId, | ||||
|                                         deviceId, | ||||
|                                         currentStory.getStoryMediaId().split("_")[0], | ||||
|                                         slider.getId(), | ||||
|                                         sliderValue, | ||||
|                                         new ServiceCallback<StoryStickerResponse>() { | ||||
|                                             @Override | ||||
|                                             public void onSuccess(final StoryStickerResponse result) { | ||||
|                                                 sticking = false; | ||||
|                                                 try { | ||||
|                                                     slider.setMyChoice(sliderValue); | ||||
|                                                     Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); | ||||
|                                                 } catch (Exception ignored) {} | ||||
|                                             } | ||||
| 
 | ||||
|                                             @Override | ||||
|                                             public void onFailure(final Throwable t) { | ||||
|                                                 sticking = false; | ||||
|                                                 Log.e(TAG, "Error responding", t); | ||||
|                                                 try { | ||||
|                                                     Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); | ||||
|                                                 } catch (Exception ignored) {} | ||||
|                                             } | ||||
|                                         }); | ||||
|                                         CoroutineUtilsKt.getContinuation( | ||||
|                                                 (storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                                     if (throwable != null) { | ||||
|                                                         sticking = false; | ||||
|                                                         Log.e(TAG, "Error responding", throwable); | ||||
|                                                         try { | ||||
|                                                             Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); | ||||
|                                                         } catch (Exception ignored) {} | ||||
|                                                         return; | ||||
|                                                     } | ||||
|                                                     sticking = false; | ||||
|                                                     try { | ||||
|                                                         slider.setMyChoice(sliderValue); | ||||
|                                                         Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); | ||||
|                                                     } catch (Exception ignored) {} | ||||
|                                                 }), Dispatchers.getIO() | ||||
|                                         ) | ||||
|                                 ); | ||||
|                             }) | ||||
|                             .setNegativeButton(R.string.cancel, null) | ||||
|                             .show(); | ||||
| @ -786,27 +805,26 @@ public class StoryViewerFragment extends Fragment { | ||||
|         setTitle(type); | ||||
|         storiesViewModel.getList().setValue(Collections.emptyList()); | ||||
|         if (type == Type.STORY) { | ||||
|             storiesService.fetch(options.getId(), new ServiceCallback<StoryModel>() { | ||||
|                 @Override | ||||
|                 public void onSuccess(final StoryModel storyModel) { | ||||
|                     fetching = false; | ||||
|                     binding.storiesList.setVisibility(View.GONE); | ||||
|                     if (storyModel == null) { | ||||
|                         storiesViewModel.getList().setValue(Collections.emptyList()); | ||||
|                         currentStory = null; | ||||
|                         return; | ||||
|                     } | ||||
|                     storiesViewModel.getList().setValue(Collections.singletonList(storyModel)); | ||||
|                     currentStory = storyModel; | ||||
|                     refreshStory(); | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void onFailure(final Throwable t) { | ||||
|                     Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); | ||||
|                     Log.e(TAG, "Error", t); | ||||
|                 } | ||||
|             }); | ||||
|             storiesService.fetch( | ||||
|                     options.getId(), | ||||
|                     CoroutineUtilsKt.getContinuation((storyModel, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                         if (throwable != null) { | ||||
|                             Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show(); | ||||
|                             Log.e(TAG, "Error", throwable); | ||||
|                             return; | ||||
|                         } | ||||
|                         fetching = false; | ||||
|                         binding.storiesList.setVisibility(View.GONE); | ||||
|                         if (storyModel == null) { | ||||
|                             storiesViewModel.getList().setValue(Collections.emptyList()); | ||||
|                             currentStory = null; | ||||
|                             return; | ||||
|                         } | ||||
|                         storiesViewModel.getList().setValue(Collections.singletonList(storyModel)); | ||||
|                         currentStory = storyModel; | ||||
|                         refreshStory(); | ||||
|                     }), Dispatchers.getIO()) | ||||
|             ); | ||||
|             return; | ||||
|         } | ||||
|         if (currentStoryMediaId == null) return; | ||||
| @ -840,7 +858,17 @@ public class StoryViewerFragment extends Fragment { | ||||
|             storyCallback.onSuccess(Collections.singletonList(live)); | ||||
|             return; | ||||
|         } | ||||
|         storiesService.getUserStory(fetchOptions, storyCallback); | ||||
|         storiesService.getUserStory( | ||||
|                 fetchOptions, | ||||
|                 CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                     if (throwable != null) { | ||||
|                         storyCallback.onFailure(throwable); | ||||
|                         return; | ||||
|                     } | ||||
|                     //noinspection unchecked | ||||
|                     storyCallback.onSuccess((List<StoryModel>) storyModels); | ||||
|                 }), Dispatchers.getIO()) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private void setTitle(final Type type) { | ||||
| @ -944,10 +972,15 @@ public class StoryViewerFragment extends Fragment { | ||||
|         } | ||||
| 
 | ||||
|         if (settingsHelper.getBoolean(MARK_AS_SEEN)) | ||||
|             storiesService.seen(currentStory.getStoryMediaId(), | ||||
|                                 currentStory.getTimestamp(), | ||||
|                                 System.currentTimeMillis() / 1000, | ||||
|                                 null); | ||||
|             storiesService.seen( | ||||
|                     csrfToken, | ||||
|                     userId, | ||||
|                     deviceId, | ||||
|                     currentStory.getStoryMediaId(), | ||||
|                     currentStory.getTimestamp(), | ||||
|                     System.currentTimeMillis() / 1000, | ||||
|                     CoroutineUtilsKt.getContinuation((s, throwable) -> {}, Dispatchers.getIO()) | ||||
|             ); | ||||
|     } | ||||
| 
 | ||||
|     private void downloadStory() { | ||||
|  | ||||
| @ -296,8 +296,8 @@ class DirectMessageSettingsFragment : Fragment(), ConfirmDialogFragmentCallback | ||||
|         usersAdapter = DirectUsersAdapter( | ||||
|             inviter?.pk ?: -1, | ||||
|             { _: Int, user: User, _: Boolean -> | ||||
|                 if (isEmpty(user.username) && !isEmpty(user.fbId)) { | ||||
|                     Utils.openURL(context, "https://facebook.com/" + user.fbId) | ||||
|                 if (user.username.isBlank() && !user.interopMessagingUserFbid.isNullOrBlank()) { | ||||
|                     Utils.openURL(context, "https://facebook.com/" + user.interopMessagingUserFbid) | ||||
|                     return@DirectUsersAdapter | ||||
|                 } | ||||
|                 if (isEmpty(user.username)) return@DirectUsersAdapter | ||||
|  | ||||
| @ -31,8 +31,6 @@ import awais.instagrabber.repositories.responses.Media; | ||||
| import awais.instagrabber.repositories.responses.discover.TopicCluster; | ||||
| import awais.instagrabber.repositories.responses.discover.TopicalExploreFeedResponse; | ||||
| import awais.instagrabber.utils.AppExecutors; | ||||
| import awais.instagrabber.utils.Constants; | ||||
| import awais.instagrabber.utils.CookieUtils; | ||||
| import awais.instagrabber.utils.CoroutineUtilsKt; | ||||
| import awais.instagrabber.utils.Utils; | ||||
| import awais.instagrabber.viewmodels.TopicClusterViewModel; | ||||
| @ -57,11 +55,11 @@ public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnR | ||||
|         super.onCreate(savedInstanceState); | ||||
|         fragmentActivity = (MainActivity) requireActivity(); | ||||
|         discoverService = DiscoverService.getInstance(); | ||||
|         final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); | ||||
|         final String cookie = Utils.settingsHelper.getString(Constants.COOKIE); | ||||
|         final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); | ||||
|         final long userId = CookieUtils.getUserIdFromCookie(cookie); | ||||
|         mediaService = MediaService.getInstance(deviceUuid, csrfToken, userId); | ||||
|         // final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); | ||||
|         // final String cookie = Utils.settingsHelper.getString(Constants.COOKIE); | ||||
|         // final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); | ||||
|         // final long userId = CookieUtils.getUserIdFromCookie(cookie); | ||||
|         mediaService = MediaService.INSTANCE; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | ||||
| @ -48,12 +48,14 @@ import awais.instagrabber.models.FeedStoryModel; | ||||
| import awais.instagrabber.models.PostsLayoutPreferences; | ||||
| import awais.instagrabber.repositories.requests.StoryViewerOptions; | ||||
| import awais.instagrabber.repositories.responses.Media; | ||||
| import awais.instagrabber.utils.AppExecutors; | ||||
| import awais.instagrabber.utils.Constants; | ||||
| import awais.instagrabber.utils.CoroutineUtilsKt; | ||||
| import awais.instagrabber.utils.DownloadUtils; | ||||
| import awais.instagrabber.utils.Utils; | ||||
| import awais.instagrabber.viewmodels.FeedStoriesViewModel; | ||||
| import awais.instagrabber.webservices.ServiceCallback; | ||||
| import awais.instagrabber.webservices.StoriesService; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| 
 | ||||
| import static androidx.core.content.PermissionChecker.checkSelfPermission; | ||||
| import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; | ||||
| @ -274,7 +276,7 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre | ||||
|     public void onCreate(@Nullable final Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         fragmentActivity = (MainActivity) requireActivity(); | ||||
|         storiesService = StoriesService.getInstance(null, 0L, null); | ||||
|         storiesService = StoriesService.INSTANCE; | ||||
|         setHasOptionsMenu(true); | ||||
|     } | ||||
| 
 | ||||
| @ -428,23 +430,23 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre | ||||
|         // final String cookie = settingsHelper.getString(Constants.COOKIE); | ||||
|         storiesFetching = true; | ||||
|         updateSwipeRefreshState(); | ||||
|         storiesService.getFeedStories(new ServiceCallback<List<FeedStoryModel>>() { | ||||
|             @Override | ||||
|             public void onSuccess(final List<FeedStoryModel> result) { | ||||
|                 storiesFetching = false; | ||||
|                 feedStoriesViewModel.getList().postValue(result); | ||||
|                 feedStoriesAdapter.submitList(result); | ||||
|                 if (storyListMenu != null) storyListMenu.setVisible(true); | ||||
|                 updateSwipeRefreshState(); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(final Throwable t) { | ||||
|                 Log.e(TAG, "failed", t); | ||||
|                 storiesFetching = false; | ||||
|                 updateSwipeRefreshState(); | ||||
|             } | ||||
|         }); | ||||
|         storiesService.getFeedStories( | ||||
|                 CoroutineUtilsKt.getContinuation((feedStoryModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                     if (throwable != null) { | ||||
|                         Log.e(TAG, "failed", throwable); | ||||
|                         storiesFetching = false; | ||||
|                         updateSwipeRefreshState(); | ||||
|                         return; | ||||
|                     } | ||||
|                     storiesFetching = false; | ||||
|                     //noinspection unchecked | ||||
|                     feedStoriesViewModel.getList().postValue((List<FeedStoryModel>) feedStoryModels); | ||||
|                     //noinspection unchecked | ||||
|                     feedStoriesAdapter.submitList((List<FeedStoryModel>) feedStoryModels); | ||||
|                     if (storyListMenu != null) storyListMenu.setVisible(true); | ||||
|                     updateSwipeRefreshState(); | ||||
|                 }), Dispatchers.getIO()) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private void showPostsLayoutPreferences() { | ||||
|  | ||||
| @ -62,7 +62,6 @@ import awais.instagrabber.databinding.FragmentProfileBinding; | ||||
| import awais.instagrabber.databinding.LayoutProfileDetailsBinding; | ||||
| import awais.instagrabber.db.datasources.AccountDataSource; | ||||
| import awais.instagrabber.db.datasources.FavoriteDataSource; | ||||
| import awais.instagrabber.db.entities.Account; | ||||
| import awais.instagrabber.db.entities.Favorite; | ||||
| import awais.instagrabber.db.repositories.AccountRepository; | ||||
| import awais.instagrabber.db.repositories.FavoriteRepository; | ||||
| @ -74,12 +73,10 @@ import awais.instagrabber.managers.DirectMessagesManager; | ||||
| import awais.instagrabber.managers.InboxManager; | ||||
| import awais.instagrabber.models.HighlightModel; | ||||
| import awais.instagrabber.models.PostsLayoutPreferences; | ||||
| import awais.instagrabber.models.StoryModel; | ||||
| import awais.instagrabber.models.enums.FavoriteType; | ||||
| import awais.instagrabber.models.enums.PostItemType; | ||||
| import awais.instagrabber.repositories.requests.StoryViewerOptions; | ||||
| import awais.instagrabber.repositories.responses.FriendshipChangeResponse; | ||||
| import awais.instagrabber.repositories.responses.FriendshipRestrictResponse; | ||||
| import awais.instagrabber.repositories.responses.FriendshipStatus; | ||||
| import awais.instagrabber.repositories.responses.Media; | ||||
| import awais.instagrabber.repositories.responses.User; | ||||
| @ -93,6 +90,7 @@ import awais.instagrabber.utils.TextUtils; | ||||
| import awais.instagrabber.utils.Utils; | ||||
| import awais.instagrabber.viewmodels.AppStateViewModel; | ||||
| import awais.instagrabber.viewmodels.HighlightsViewModel; | ||||
| import awais.instagrabber.viewmodels.ProfileFragmentViewModel; | ||||
| import awais.instagrabber.webservices.DirectMessagesService; | ||||
| import awais.instagrabber.webservices.FriendshipService; | ||||
| import awais.instagrabber.webservices.GraphQLService; | ||||
| @ -139,6 +137,14 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|     private int downloadChildPosition = -1; | ||||
|     private long myId; | ||||
|     private PostsLayoutPreferences layoutPreferences = Utils.getPostsLayoutPreferences(Constants.PREF_PROFILE_POSTS_LAYOUT); | ||||
|     private LayoutProfileDetailsBinding profileDetailsBinding; | ||||
|     private AccountRepository accountRepository; | ||||
|     private FavoriteRepository favoriteRepository; | ||||
|     private AppStateViewModel appStateViewModel; | ||||
|     private boolean disableDm = false; | ||||
|     private ProfileFragmentViewModel viewModel; | ||||
|     private String csrfToken; | ||||
|     private String deviceUuid; | ||||
| 
 | ||||
|     private final ServiceCallback<FriendshipChangeResponse> changeCb = new ServiceCallback<FriendshipChangeResponse>() { | ||||
|         @Override | ||||
| @ -156,7 +162,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|             Log.e(TAG, "Error editing relationship", t); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     private final Runnable usernameSettingRunnable = () -> { | ||||
|         final ActionBar actionBar = fragmentActivity.getSupportActionBar(); | ||||
|         if (actionBar != null && !TextUtils.isEmpty(username)) { | ||||
| @ -318,11 +323,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|     private LayoutProfileDetailsBinding profileDetailsBinding; | ||||
|     private AccountRepository accountRepository; | ||||
|     private FavoriteRepository favoriteRepository; | ||||
|     private AppStateViewModel appStateViewModel; | ||||
|     private boolean disableDm = false; | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate(@Nullable final Bundle savedInstanceState) { | ||||
| @ -330,20 +330,21 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|         cookie = Utils.settingsHelper.getString(Constants.COOKIE); | ||||
|         isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; | ||||
|         myId = CookieUtils.getUserIdFromCookie(cookie); | ||||
|         final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); | ||||
|         final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); | ||||
|         deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); | ||||
|         csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); | ||||
|         fragmentActivity = (MainActivity) requireActivity(); | ||||
|         friendshipService = isLoggedIn ? FriendshipService.getInstance(deviceUuid, csrfToken, myId) : null; | ||||
|         directMessagesService = isLoggedIn ? DirectMessagesService.getInstance(csrfToken, myId, deviceUuid) : null; | ||||
|         storiesService = isLoggedIn ? StoriesService.getInstance(null, 0L, null) : null; | ||||
|         mediaService = isLoggedIn ? MediaService.getInstance(deviceUuid, csrfToken, myId) : null; | ||||
|         userService = isLoggedIn ? UserService.getInstance() : null; | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); | ||||
|         friendshipService = isLoggedIn ? FriendshipService.INSTANCE : null; | ||||
|         directMessagesService = isLoggedIn ? DirectMessagesService.INSTANCE : null; | ||||
|         storiesService = isLoggedIn ? StoriesService.INSTANCE : null; | ||||
|         mediaService = isLoggedIn ? MediaService.INSTANCE : null; | ||||
|         userService = isLoggedIn ? UserService.INSTANCE : null; | ||||
|         graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; | ||||
|         final Context context = getContext(); | ||||
|         if (context == null) return; | ||||
|         accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(context)); | ||||
|         favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(context)); | ||||
|         appStateViewModel = new ViewModelProvider(fragmentActivity).get(AppStateViewModel.class); | ||||
|         viewModel = new ViewModelProvider(this).get(ProfileFragmentViewModel.class); | ||||
|         setHasOptionsMenu(true); | ||||
|     } | ||||
| 
 | ||||
| @ -373,6 +374,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|             shouldRefresh = false; | ||||
|             return root; | ||||
|         } | ||||
|         // appStateViewModel.getCurrentUserLiveData().observe(getViewLifecycleOwner(), user -> viewModel.setCurrentUser(user)); | ||||
|         binding = FragmentProfileBinding.inflate(inflater, container, false); | ||||
|         root = binding.getRoot(); | ||||
|         profileDetailsBinding = binding.header; | ||||
| @ -430,7 +432,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|         } | ||||
|         chainingMenuItem = menu.findItem(R.id.chaining); | ||||
|         if (chainingMenuItem != null) { | ||||
|             chainingMenuItem.setVisible(isNotMe && profileModel.hasChaining()); | ||||
|             chainingMenuItem.setVisible(isNotMe && profileModel.getHasChaining()); | ||||
|         } | ||||
|         removeFollowerMenuItem = menu.findItem(R.id.remove_follower); | ||||
|         if (removeFollowerMenuItem != null) { | ||||
| @ -448,25 +450,38 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|             if (!isLoggedIn) return false; | ||||
|             final String action = profileModel.getFriendshipStatus().isRestricted() ? "Unrestrict" : "Restrict"; | ||||
|             friendshipService.toggleRestrict( | ||||
|                     csrfToken, | ||||
|                     deviceUuid, | ||||
|                     profileModel.getPk(), | ||||
|                     !profileModel.getFriendshipStatus().isRestricted(), | ||||
|                     new ServiceCallback<FriendshipRestrictResponse>() { | ||||
|                         @Override | ||||
|                         public void onSuccess(final FriendshipRestrictResponse result) { | ||||
|                             Log.d(TAG, action + " success: " + result); | ||||
|                             fetchProfileDetails(); | ||||
|                     CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                         if (throwable != null) { | ||||
|                             Log.e(TAG, "Error while performing " + action, throwable); | ||||
|                             return; | ||||
|                         } | ||||
| 
 | ||||
|                         @Override | ||||
|                         public void onFailure(final Throwable t) { | ||||
|                             Log.e(TAG, "Error while performing " + action, t); | ||||
|                         } | ||||
|                     }); | ||||
|                         // Log.d(TAG, action + " success: " + response); | ||||
|                         fetchProfileDetails(); | ||||
|                     }), Dispatchers.getIO()) | ||||
|             ); | ||||
|             return true; | ||||
|         } | ||||
|         if (item.getItemId() == R.id.block) { | ||||
|             if (!isLoggedIn) return false; | ||||
|             friendshipService.changeBlock(profileModel.getFriendshipStatus().getBlocking(), profileModel.getPk(), changeCb); | ||||
|             // changeCb | ||||
|             friendshipService.changeBlock( | ||||
|                     csrfToken, | ||||
|                     myId, | ||||
|                     deviceUuid, | ||||
|                     profileModel.getFriendshipStatus().getBlocking(), | ||||
|                     profileModel.getPk(), | ||||
|                     CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                         if (throwable != null) { | ||||
|                             changeCb.onFailure(throwable); | ||||
|                             return; | ||||
|                         } | ||||
|                         changeCb.onSuccess(response); | ||||
|                     }), Dispatchers.getIO()) | ||||
|             ); | ||||
|             return true; | ||||
|         } | ||||
|         if (item.getItemId() == R.id.chaining) { | ||||
| @ -481,25 +496,57 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|             if (!isLoggedIn) return false; | ||||
|             final String action = profileModel.getFriendshipStatus().isMutingReel() ? "Unmute stories" : "Mute stories"; | ||||
|             friendshipService.changeMute( | ||||
|                     csrfToken, | ||||
|                     myId, | ||||
|                     deviceUuid, | ||||
|                     profileModel.getFriendshipStatus().isMutingReel(), | ||||
|                     profileModel.getPk(), | ||||
|                     true, | ||||
|                     changeCb); | ||||
|                     CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                         if (throwable != null) { | ||||
|                             changeCb.onFailure(throwable); | ||||
|                             return; | ||||
|                         } | ||||
|                         changeCb.onSuccess(response); | ||||
|                     }), Dispatchers.getIO()) | ||||
|             ); | ||||
|             return true; | ||||
|         } | ||||
|         if (item.getItemId() == R.id.mute_posts) { | ||||
|             if (!isLoggedIn) return false; | ||||
|             final String action = profileModel.getFriendshipStatus().getMuting() ? "Unmute stories" : "Mute stories"; | ||||
|             friendshipService.changeMute( | ||||
|                     csrfToken, | ||||
|                     myId, | ||||
|                     deviceUuid, | ||||
|                     profileModel.getFriendshipStatus().getMuting(), | ||||
|                     profileModel.getPk(), | ||||
|                     false, | ||||
|                     changeCb); | ||||
|                     CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                         if (throwable != null) { | ||||
|                             changeCb.onFailure(throwable); | ||||
|                             return; | ||||
|                         } | ||||
|                         changeCb.onSuccess(response); | ||||
|                     }), Dispatchers.getIO()) | ||||
|             ); | ||||
|             return true; | ||||
|         } | ||||
|         if (item.getItemId() == R.id.remove_follower) { | ||||
|             if (!isLoggedIn) return false; | ||||
|             friendshipService.removeFollower(profileModel.getPk(), changeCb); | ||||
|             friendshipService.removeFollower( | ||||
|                     csrfToken, | ||||
|                     myId, | ||||
|                     deviceUuid, | ||||
|                     profileModel.getPk(), | ||||
|                     CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                         if (throwable != null) { | ||||
|                             changeCb.onFailure(throwable); | ||||
|                             return; | ||||
|                         } | ||||
|                         changeCb.onSuccess(response); | ||||
|                     }), Dispatchers.getIO()) | ||||
|             ); | ||||
|             return true; | ||||
|         } | ||||
|         return super.onOptionsItemSelected(item); | ||||
| @ -583,65 +630,51 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|             return; | ||||
|         } | ||||
|         if (isLoggedIn) { | ||||
|             userService.getUsernameInfo(usernameTemp, new ServiceCallback<User>() { | ||||
|                 @Override | ||||
|                 public void onSuccess(final User user) { | ||||
|                     userService.getUserFriendship(user.getPk(), new ServiceCallback<FriendshipStatus>() { | ||||
|                         @Override | ||||
|                         public void onSuccess(final FriendshipStatus status) { | ||||
|                             user.setFriendshipStatus(status); | ||||
|                             profileModel = user; | ||||
|                             setProfileDetails(); | ||||
|                         } | ||||
| 
 | ||||
|                         @Override | ||||
|                         public void onFailure(final Throwable t) { | ||||
|                             Log.e(TAG, "Error fetching profile relationship", t); | ||||
|             userService.getUsernameInfo( | ||||
|                     usernameTemp, | ||||
|                     CoroutineUtilsKt.getContinuation((user, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                         if (throwable != null) { | ||||
|                             Log.e(TAG, "Error fetching profile", throwable); | ||||
|                             final Context context = getContext(); | ||||
|                             try { | ||||
|                                 if (t == null) | ||||
|                                     Toast.makeText(context, R.string.error_loading_profile_loggedin, Toast.LENGTH_LONG).show(); | ||||
|                                 else | ||||
|                                     Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); | ||||
|                             } catch (final Throwable ignored) { | ||||
|                             } | ||||
|                             if (context == null) return; | ||||
|                             Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show(); | ||||
|                             return; | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void onFailure(final Throwable t) { | ||||
|                     Log.e(TAG, "Error fetching profile", t); | ||||
|                     final Context context = getContext(); | ||||
|                     try { | ||||
|                         if (t == null) | ||||
|                             Toast.makeText(context, R.string.error_loading_profile_loggedin, Toast.LENGTH_LONG).show(); | ||||
|                         else Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); | ||||
|                     } catch (final Throwable ignored) { | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|                         userService.getUserFriendship( | ||||
|                                 user.getPk(), | ||||
|                                 CoroutineUtilsKt.getContinuation( | ||||
|                                         (friendshipStatus, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                             if (throwable1 != null) { | ||||
|                                                 Log.e(TAG, "Error fetching profile relationship", throwable1); | ||||
|                                                 final Context context = getContext(); | ||||
|                                                 if (context == null) return; | ||||
|                                                 Toast.makeText(context, throwable1.getMessage(), | ||||
|                                                                Toast.LENGTH_SHORT).show(); | ||||
|                                                 return; | ||||
|                                             } | ||||
|                                             user.setFriendshipStatus(friendshipStatus); | ||||
|                                             profileModel = user; | ||||
|                                             setProfileDetails(); | ||||
|                                         }), Dispatchers.getIO() | ||||
|                                 ) | ||||
|                         ); | ||||
|                     }), Dispatchers.getIO()) | ||||
|             ); | ||||
|             return; | ||||
|         } | ||||
|         graphQLService.fetchUser(usernameTemp, new ServiceCallback<User>() { | ||||
|             @Override | ||||
|             public void onSuccess(final User user) { | ||||
|                 profileModel = user; | ||||
|                 setProfileDetails(); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(final Throwable t) { | ||||
|                 Log.e(TAG, "Error fetching profile", t); | ||||
|                 final Context context = getContext(); | ||||
|                 try { | ||||
|                     if (t == null) | ||||
|                         Toast.makeText(context, R.string.error_loading_profile, Toast.LENGTH_LONG).show(); | ||||
|                     else Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); | ||||
|                 } catch (final Throwable ignored) { | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|         graphQLService.fetchUser( | ||||
|                 usernameTemp, | ||||
|                 CoroutineUtilsKt.getContinuation((user, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                     if (throwable != null) { | ||||
|                         Log.e(TAG, "Error fetching profile", throwable); | ||||
|                         final Context context = getContext(); | ||||
|                         if (context == null) return; | ||||
|                         Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show(); | ||||
|                     } | ||||
|                     profileModel = user; | ||||
|                     setProfileDetails(); | ||||
|                 })) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private void setProfileDetails() { | ||||
| @ -856,7 +889,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|             profileDetailsBinding.profileContext.setVisibility(View.GONE); | ||||
|         } else { | ||||
|             profileDetailsBinding.profileContext.setVisibility(View.VISIBLE); | ||||
|             final List<UserProfileContextLink> userProfileContextLinks = profileModel.getProfileContextLinks(); | ||||
|             final List<UserProfileContextLink> userProfileContextLinks = profileModel.getProfileContextLinksWithUserIds(); | ||||
|             for (int i = 0; i < userProfileContextLinks.size(); i++) { | ||||
|                 final UserProfileContextLink link = userProfileContextLinks.get(i); | ||||
|                 if (link.getUsername() != null) | ||||
| @ -977,7 +1010,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|                 mutePostsMenuItem.setTitle(profileModel.getFriendshipStatus().getMuting() ? R.string.unmute_posts : R.string.mute_posts); | ||||
|             } | ||||
|             if (chainingMenuItem != null) { | ||||
|                 chainingMenuItem.setVisible(profileModel.hasChaining()); | ||||
|                 chainingMenuItem.setVisible(profileModel.getHasChaining()); | ||||
|             } | ||||
|             if (removeFollowerMenuItem != null) { | ||||
|                 removeFollowerMenuItem.setVisible(profileModel.getFriendshipStatus().getFollowedBy()); | ||||
| @ -993,69 +1026,100 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|                 cookie, | ||||
|                 profileModel.getFullName(), | ||||
|                 profileModel.getProfilePicUrl(), | ||||
|                 new RepositoryCallback<Account>() { | ||||
|                     @Override | ||||
|                     public void onSuccess(final Account result) { | ||||
|                         accountIsUpdated = true; | ||||
|                 CoroutineUtilsKt.getContinuation((account, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                     if (throwable != null) { | ||||
|                         Log.e(TAG, "updateAccountInfo: ", throwable); | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     @Override | ||||
|                     public void onDataNotAvailable() { | ||||
|                         Log.e(TAG, "onDataNotAvailable: insert failed"); | ||||
|                     } | ||||
|                 }); | ||||
|                     accountIsUpdated = true; | ||||
|                 }), Dispatchers.getIO()) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private void fetchStoryAndHighlights(final long profileId) { | ||||
|         storiesService.getUserStory( | ||||
|                 StoryViewerOptions.forUser(profileId, profileModel.getFullName()), | ||||
|                 new ServiceCallback<List<StoryModel>>() { | ||||
|                     @Override | ||||
|                     public void onSuccess(final List<StoryModel> storyModels) { | ||||
|                         if (storyModels != null && !storyModels.isEmpty()) { | ||||
|                             profileDetailsBinding.mainProfileImage.setStoriesBorder(1); | ||||
|                             hasStories = true; | ||||
|                         } | ||||
|                 CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                     if (throwable != null) { | ||||
|                         Log.e(TAG, "Error", throwable); | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     @Override | ||||
|                     public void onFailure(final Throwable t) { | ||||
|                         Log.e(TAG, "Error", t); | ||||
|                     if (storyModels != null && !storyModels.isEmpty()) { | ||||
|                         profileDetailsBinding.mainProfileImage.setStoriesBorder(1); | ||||
|                         hasStories = true; | ||||
|                     } | ||||
|                 }); | ||||
|         storiesService.fetchHighlights(profileId, | ||||
|                                        new ServiceCallback<List<HighlightModel>>() { | ||||
|                                            @Override | ||||
|                                            public void onSuccess(final List<HighlightModel> result) { | ||||
|                                                if (result != null) { | ||||
|                                                    profileDetailsBinding.highlightsList.setVisibility(View.VISIBLE); | ||||
|                                                    highlightsViewModel.getList().postValue(result); | ||||
|                                                } else profileDetailsBinding.highlightsList.setVisibility(View.GONE); | ||||
|                                            } | ||||
| 
 | ||||
|                                            @Override | ||||
|                                            public void onFailure(final Throwable t) { | ||||
|                                                profileDetailsBinding.highlightsList.setVisibility(View.GONE); | ||||
|                                                Log.e(TAG, "Error", t); | ||||
|                                            } | ||||
|                                        }); | ||||
|                 }), Dispatchers.getIO()) | ||||
|         ); | ||||
|         storiesService.fetchHighlights( | ||||
|                 profileId, | ||||
|                 CoroutineUtilsKt.getContinuation((highlightModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                     if (throwable != null) { | ||||
|                         profileDetailsBinding.highlightsList.setVisibility(View.GONE); | ||||
|                         Log.e(TAG, "Error", throwable); | ||||
|                         return; | ||||
|                     } | ||||
|                     if (highlightModels != null) { | ||||
|                         profileDetailsBinding.highlightsList.setVisibility(View.VISIBLE); | ||||
|                         //noinspection unchecked | ||||
|                         highlightsViewModel.getList().postValue((List<HighlightModel>) highlightModels); | ||||
|                     } else { | ||||
|                         profileDetailsBinding.highlightsList.setVisibility(View.GONE); | ||||
|                     } | ||||
|                 }), Dispatchers.getIO()) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private void setupCommonListeners() { | ||||
|         final Context context = getContext(); | ||||
|         if (context == null) return; | ||||
|         profileDetailsBinding.btnFollow.setOnClickListener(v -> { | ||||
|             if (profileModel.getFriendshipStatus().getFollowing() && profileModel.isPrivate()) { | ||||
|                 new AlertDialog.Builder(context) | ||||
|                         .setTitle(R.string.priv_acc) | ||||
|                         .setMessage(R.string.priv_acc_confirm) | ||||
|                         .setPositiveButton(R.string.confirm, (d, w) -> | ||||
|                                 friendshipService.unfollow(profileModel.getPk(), changeCb)) | ||||
|                         .setPositiveButton(R.string.confirm, (d, w) -> friendshipService.unfollow( | ||||
|                                 csrfToken, | ||||
|                                 myId, | ||||
|                                 deviceUuid, | ||||
|                                 profileModel.getPk(), | ||||
|                                 CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                     if (throwable != null) { | ||||
|                                         changeCb.onFailure(throwable); | ||||
|                                         return; | ||||
|                                     } | ||||
|                                     changeCb.onSuccess(response); | ||||
|                                 }), Dispatchers.getIO()) | ||||
|                         )) | ||||
|                         .setNegativeButton(R.string.cancel, null) | ||||
|                         .show(); | ||||
|             } else if (profileModel.getFriendshipStatus().getFollowing() || profileModel.getFriendshipStatus().getOutgoingRequest()) { | ||||
|                 friendshipService.unfollow(profileModel.getPk(), changeCb); | ||||
|                 friendshipService.unfollow( | ||||
|                         csrfToken, | ||||
|                         myId, | ||||
|                         deviceUuid, | ||||
|                         profileModel.getPk(), | ||||
|                         CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                             if (throwable != null) { | ||||
|                                 changeCb.onFailure(throwable); | ||||
|                                 return; | ||||
|                             } | ||||
|                             changeCb.onSuccess(response); | ||||
|                         }), Dispatchers.getIO()) | ||||
|                 ); | ||||
|             } else { | ||||
|                 friendshipService.follow(profileModel.getPk(), changeCb); | ||||
|                 friendshipService.follow( | ||||
|                         csrfToken, | ||||
|                         myId, | ||||
|                         deviceUuid, | ||||
|                         profileModel.getPk(), | ||||
|                         CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                             if (throwable != null) { | ||||
|                                 changeCb.onFailure(throwable); | ||||
|                                 return; | ||||
|                             } | ||||
|                             changeCb.onSuccess(response); | ||||
|                         }), Dispatchers.getIO()) | ||||
|                 ); | ||||
|             } | ||||
|         }); | ||||
|         profileDetailsBinding.btnSaved.setOnClickListener(v -> { | ||||
| @ -1078,6 +1142,9 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|             profileDetailsBinding.btnDM.setOnClickListener(v -> { | ||||
|                 profileDetailsBinding.btnDM.setEnabled(false); | ||||
|                 directMessagesService.createThread( | ||||
|                         csrfToken, | ||||
|                         myId, | ||||
|                         deviceUuid, | ||||
|                         Collections.singletonList(profileModel.getPk()), | ||||
|                         null, | ||||
|                         CoroutineUtilsKt.getContinuation((thread, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
| @ -1120,7 +1187,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe | ||||
|                 } | ||||
|                 showProfilePicDialog(); | ||||
|             }; | ||||
|             if (context == null) return; | ||||
|             new AlertDialog.Builder(context) | ||||
|                     .setItems(options, profileDialogListener) | ||||
|                     .setNegativeButton(R.string.cancel, null) | ||||
|  | ||||
| @ -11,7 +11,6 @@ import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.Toast; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.app.AlertDialog; | ||||
| import androidx.fragment.app.FragmentManager; | ||||
| @ -24,28 +23,24 @@ import androidx.preference.PreferenceScreen; | ||||
| import androidx.preference.PreferenceViewHolder; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| import awais.instagrabber.BuildConfig; | ||||
| import awais.instagrabber.R; | ||||
| import awais.instagrabber.activities.Login; | ||||
| import awais.instagrabber.activities.MainActivity; | ||||
| import awais.instagrabber.databinding.PrefAccountSwitcherBinding; | ||||
| import awais.instagrabber.db.datasources.AccountDataSource; | ||||
| import awais.instagrabber.db.entities.Account; | ||||
| import awais.instagrabber.db.repositories.AccountRepository; | ||||
| import awais.instagrabber.db.repositories.RepositoryCallback; | ||||
| import awais.instagrabber.dialogs.AccountSwitcherDialogFragment; | ||||
| import awais.instagrabber.repositories.responses.User; | ||||
| import awais.instagrabber.utils.AppExecutors; | ||||
| import awais.instagrabber.utils.Constants; | ||||
| import awais.instagrabber.utils.CookieUtils; | ||||
| import awais.instagrabber.utils.CoroutineUtilsKt; | ||||
| import awais.instagrabber.utils.FlavorTown; | ||||
| import awais.instagrabber.utils.ProcessPhoenix; | ||||
| import awais.instagrabber.utils.TextUtils; | ||||
| import awais.instagrabber.utils.Utils; | ||||
| import awais.instagrabber.webservices.ServiceCallback; | ||||
| import awais.instagrabber.webservices.UserService; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| 
 | ||||
| import static awais.instagrabber.utils.Utils.settingsHelper; | ||||
| 
 | ||||
| @ -98,75 +93,77 @@ public class MorePreferencesFragment extends BasePreferencesFragment { | ||||
|                 return true; | ||||
|             })); | ||||
|         } | ||||
|         accountRepository.getAllAccounts(new RepositoryCallback<List<Account>>() { | ||||
|             @Override | ||||
|             public void onSuccess(@NonNull final List<Account> accounts) { | ||||
|                 if (!isLoggedIn) { | ||||
|                     if (accounts.size() > 0) { | ||||
|                         final Context context1 = getContext(); | ||||
|                         final AccountSwitcherPreference preference = getAccountSwitcherPreference(null, context1); | ||||
|                         if (preference == null) return; | ||||
|                         accountCategory.addPreference(preference); | ||||
|                     } | ||||
|                     // Need to show something to trigger login activity | ||||
|                     final Preference preference1 = getPreference(R.string.add_account, R.drawable.ic_add, preference -> { | ||||
|                         final Context context1 = getContext(); | ||||
|                         if (context1 == null) return false; | ||||
|                         startActivityForResult(new Intent(context1, Login.class), Constants.LOGIN_RESULT_CODE); | ||||
|                         return true; | ||||
|                     }); | ||||
|                     if (preference1 == null) return; | ||||
|                     accountCategory.addPreference(preference1); | ||||
|                 } | ||||
|                 if (accounts.size() > 0) { | ||||
|                     final Preference preference1 = getPreference( | ||||
|                             R.string.remove_all_acc, | ||||
|                             null, | ||||
|                             R.drawable.ic_account_multiple_remove_24, | ||||
|                             preference -> { | ||||
|                                 if (getContext() == null) return false; | ||||
|                                 new AlertDialog.Builder(getContext()) | ||||
|                                         .setTitle(R.string.logout) | ||||
|                                         .setMessage(R.string.remove_all_acc_warning) | ||||
|                                         .setPositiveButton(R.string.yes, (dialog, which) -> { | ||||
|                                             final Context context1 = getContext(); | ||||
|                                             if (context1 == null) return; | ||||
|                                             CookieUtils.removeAllAccounts(context1, new RepositoryCallback<Void>() { | ||||
|                                                 @Override | ||||
|                                                 public void onSuccess(final Void result) { | ||||
|                                                     // shouldRecreate(); | ||||
|                                                     final Context context1 = getContext(); | ||||
|                                                     if (context1 == null) return; | ||||
|                                                     Toast.makeText(context1, R.string.logout_success, Toast.LENGTH_SHORT).show(); | ||||
|                                                     settingsHelper.putString(Constants.COOKIE, ""); | ||||
|                                                     AppExecutors.INSTANCE.getMainThread().execute(() -> ProcessPhoenix.triggerRebirth(context1), 200); | ||||
|                                                 } | ||||
| 
 | ||||
|                                                 @Override | ||||
|                                                 public void onDataNotAvailable() {} | ||||
|                                             }); | ||||
|                                         }) | ||||
|                                         .setNegativeButton(R.string.cancel, null) | ||||
|                                         .show(); | ||||
|         accountRepository.getAllAccounts( | ||||
|                 CoroutineUtilsKt.getContinuation((accounts, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                     if (throwable != null) { | ||||
|                         Log.d(TAG, "getAllAccounts", throwable); | ||||
|                         if (!isLoggedIn) { | ||||
|                             // Need to show something to trigger login activity | ||||
|                             accountCategory.addPreference(getPreference(R.string.add_account, R.drawable.ic_add, preference -> { | ||||
|                                 startActivityForResult(new Intent(getContext(), Login.class), Constants.LOGIN_RESULT_CODE); | ||||
|                                 return true; | ||||
|                             }); | ||||
|                     if (preference1 == null) return; | ||||
|                     accountCategory.addPreference(preference1); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onDataNotAvailable() { | ||||
|                 Log.d(TAG, "onDataNotAvailable"); | ||||
|                 if (!isLoggedIn) { | ||||
|                     // Need to show something to trigger login activity | ||||
|                     accountCategory.addPreference(getPreference(R.string.add_account, R.drawable.ic_add, preference -> { | ||||
|                         startActivityForResult(new Intent(getContext(), Login.class), Constants.LOGIN_RESULT_CODE); | ||||
|                         return true; | ||||
|                     })); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|                             })); | ||||
|                         } | ||||
|                         return; | ||||
|                     } | ||||
|                     if (!isLoggedIn) { | ||||
|                         if (accounts.size() > 0) { | ||||
|                             final Context context1 = getContext(); | ||||
|                             final AccountSwitcherPreference preference = getAccountSwitcherPreference(null, context1); | ||||
|                             if (preference == null) return; | ||||
|                             accountCategory.addPreference(preference); | ||||
|                         } | ||||
|                         // Need to show something to trigger login activity | ||||
|                         final Preference preference1 = getPreference(R.string.add_account, R.drawable.ic_add, preference -> { | ||||
|                             final Context context1 = getContext(); | ||||
|                             if (context1 == null) return false; | ||||
|                             startActivityForResult(new Intent(context1, Login.class), Constants.LOGIN_RESULT_CODE); | ||||
|                             return true; | ||||
|                         }); | ||||
|                         if (preference1 == null) return; | ||||
|                         accountCategory.addPreference(preference1); | ||||
|                     } | ||||
|                     if (accounts.size() > 0) { | ||||
|                         final Preference preference1 = getPreference( | ||||
|                                 R.string.remove_all_acc, | ||||
|                                 null, | ||||
|                                 R.drawable.ic_account_multiple_remove_24, | ||||
|                                 preference -> { | ||||
|                                     if (getContext() == null) return false; | ||||
|                                     new AlertDialog.Builder(getContext()) | ||||
|                                             .setTitle(R.string.logout) | ||||
|                                             .setMessage(R.string.remove_all_acc_warning) | ||||
|                                             .setPositiveButton(R.string.yes, (dialog, which) -> { | ||||
|                                                 final Context context1 = getContext(); | ||||
|                                                 if (context1 == null) return; | ||||
|                                                 CookieUtils.removeAllAccounts( | ||||
|                                                         context1, | ||||
|                                                         CoroutineUtilsKt.getContinuation( | ||||
|                                                                 (unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                                                     if (throwable1 != null) { | ||||
|                                                                         return; | ||||
|                                                                     } | ||||
|                                                                     final Context context2 = getContext(); | ||||
|                                                                     if (context2 == null) return; | ||||
|                                                                     Toast.makeText(context2, R.string.logout_success, Toast.LENGTH_SHORT).show(); | ||||
|                                                                     settingsHelper.putString(Constants.COOKIE, ""); | ||||
|                                                                     AppExecutors.INSTANCE | ||||
|                                                                             .getMainThread() | ||||
|                                                                             .execute(() -> ProcessPhoenix.triggerRebirth(context1), 200); | ||||
|                                                                 }), | ||||
|                                                                 Dispatchers.getIO() | ||||
|                                                         ) | ||||
|                                                 ); | ||||
|                                             }) | ||||
|                                             .setNegativeButton(R.string.cancel, null) | ||||
|                                             .show(); | ||||
|                                     return true; | ||||
|                                 }); | ||||
|                         if (preference1 == null) return; | ||||
|                         accountCategory.addPreference(preference1); | ||||
|                     } | ||||
|                 }), Dispatchers.getIO()) | ||||
|         ); | ||||
| 
 | ||||
|         // final PreferenceCategory generalCategory = new PreferenceCategory(context); | ||||
|         // generalCategory.setTitle(R.string.pref_category_general); | ||||
| @ -288,44 +285,33 @@ public class MorePreferencesFragment extends BasePreferencesFragment { | ||||
| 
 | ||||
|             // adds cookies to database for quick access | ||||
|             final long uid = CookieUtils.getUserIdFromCookie(cookie); | ||||
|             final UserService userService = UserService.getInstance(); | ||||
|             userService.getUserInfo(uid, new ServiceCallback<User>() { | ||||
|                 @Override | ||||
|                 public void onSuccess(final User result) { | ||||
|                     // Log.d(TAG, "adding userInfo: " + result); | ||||
|                     if (result != null) { | ||||
|                         accountRepository.insertOrUpdateAccount( | ||||
|                                 uid, | ||||
|                                 result.getUsername(), | ||||
|                                 cookie, | ||||
|                                 result.getFullName(), | ||||
|                                 result.getProfilePicUrl(), | ||||
|                                 new RepositoryCallback<Account>() { | ||||
|                                     @Override | ||||
|                                     public void onSuccess(final Account result) { | ||||
|                                         // final FragmentActivity activity = getActivity(); | ||||
|                                         // if (activity == null) return; | ||||
|                                         // activity.recreate(); | ||||
|                                         AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                             final Context context = getContext(); | ||||
|                                             if (context == null) return; | ||||
|                                             ProcessPhoenix.triggerRebirth(context); | ||||
|                                         }, 200); | ||||
|                                     } | ||||
| 
 | ||||
|                                     @Override | ||||
|                                     public void onDataNotAvailable() { | ||||
|                                         Log.e(TAG, "onDataNotAvailable: insert failed"); | ||||
|                                     } | ||||
|                                 }); | ||||
|                     } | ||||
|             final UserService userService = UserService.INSTANCE; | ||||
|             userService.getUserInfo(uid, CoroutineUtilsKt.getContinuation((user, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                 if (throwable != null) { | ||||
|                     Log.e(TAG, "Error fetching user info", throwable); | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void onFailure(final Throwable t) { | ||||
|                     Log.e(TAG, "Error fetching user info", t); | ||||
|                 if (user != null) { | ||||
|                     accountRepository.insertOrUpdateAccount( | ||||
|                             uid, | ||||
|                             user.getUsername(), | ||||
|                             cookie, | ||||
|                             user.getFullName(), | ||||
|                             user.getProfilePicUrl(), | ||||
|                             CoroutineUtilsKt.getContinuation((account, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                 if (throwable1 != null) { | ||||
|                                     Log.e(TAG, "onActivityResult: ", throwable1); | ||||
|                                     return; | ||||
|                                 } | ||||
|                                 AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                                     final Context context = getContext(); | ||||
|                                     if (context == null) return; | ||||
|                                     ProcessPhoenix.triggerRebirth(context); | ||||
|                                 }, 200); | ||||
|                             }), Dispatchers.getIO()) | ||||
|                     ); | ||||
|                 } | ||||
|             }); | ||||
|             }), Dispatchers.getIO())); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -419,20 +405,21 @@ public class MorePreferencesFragment extends BasePreferencesFragment { | ||||
|             final PrefAccountSwitcherBinding binding = PrefAccountSwitcherBinding.bind(root); | ||||
|             final long uid = CookieUtils.getUserIdFromCookie(cookie); | ||||
|             if (uid <= 0) return; | ||||
|             accountRepository.getAccount(uid, new RepositoryCallback<Account>() { | ||||
|                 @Override | ||||
|                 public void onSuccess(final Account account) { | ||||
|                     binding.getRoot().post(() -> { | ||||
|                         binding.fullName.setText(account.getFullName()); | ||||
|                         binding.username.setText("@" + account.getUsername()); | ||||
|                         binding.profilePic.setImageURI(account.getProfilePic()); | ||||
|                         binding.getRoot().requestLayout(); | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void onDataNotAvailable() {} | ||||
|             }); | ||||
|             accountRepository.getAccount( | ||||
|                     uid, | ||||
|                     CoroutineUtilsKt.getContinuation((account, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                         if (throwable != null) { | ||||
|                             Log.e(TAG, "onBindViewHolder: ", throwable); | ||||
|                             return; | ||||
|                         } | ||||
|                         binding.getRoot().post(() -> { | ||||
|                             binding.fullName.setText(account.getFullName()); | ||||
|                             binding.username.setText("@" + account.getUsername()); | ||||
|                             binding.profilePic.setImageURI(account.getProfilePic()); | ||||
|                             binding.getRoot().requestLayout(); | ||||
|                         }); | ||||
|                     }), Dispatchers.getIO()) | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -4,7 +4,6 @@ import android.content.ContentResolver | ||||
| import android.util.Log | ||||
| import androidx.lifecycle.LiveData | ||||
| import androidx.lifecycle.MutableLiveData | ||||
| import awais.instagrabber.managers.ThreadManager.Companion.getInstance | ||||
| import awais.instagrabber.models.Resource | ||||
| import awais.instagrabber.models.Resource.Companion.error | ||||
| import awais.instagrabber.models.Resource.Companion.loading | ||||
| @ -18,21 +17,19 @@ import awais.instagrabber.utils.Utils | ||||
| import awais.instagrabber.utils.getCsrfTokenFromCookie | ||||
| import awais.instagrabber.utils.getUserIdFromCookie | ||||
| import awais.instagrabber.webservices.DirectMessagesService | ||||
| import awais.instagrabber.webservices.DirectMessagesService.Companion.getInstance | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.launch | ||||
| import java.util.* | ||||
| 
 | ||||
| object DirectMessagesManager { | ||||
|     val inboxManager: InboxManager by lazy { InboxManager.getInstance(false) } | ||||
|     val pendingInboxManager: InboxManager by lazy { InboxManager.getInstance(true) } | ||||
|     val inboxManager: InboxManager by lazy { InboxManager(false) } | ||||
|     val pendingInboxManager: InboxManager by lazy { InboxManager(true) } | ||||
| 
 | ||||
|     private val TAG = DirectMessagesManager::class.java.simpleName | ||||
|     private val viewerId: Long | ||||
|     private val deviceUuid: String | ||||
|     private val csrfToken: String | ||||
|     private val service: DirectMessagesService | ||||
| 
 | ||||
|     fun moveThreadFromPending(threadId: String) { | ||||
|         val pendingThreads = pendingInboxManager.threads.value ?: return | ||||
| @ -65,10 +62,10 @@ object DirectMessagesManager { | ||||
|         currentUser: User, | ||||
|         contentResolver: ContentResolver, | ||||
|     ): ThreadManager { | ||||
|         return getInstance(threadId, pending, currentUser, contentResolver, viewerId, csrfToken, deviceUuid) | ||||
|         return ThreadManager(threadId, pending, currentUser, contentResolver, viewerId, csrfToken, deviceUuid) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun createThread(userPk: Long): DirectThread = service.createThread(listOf(userPk), null) | ||||
|     suspend fun createThread(userPk: Long): DirectThread = DirectMessagesService.createThread(csrfToken, viewerId, deviceUuid, listOf(userPk), null) | ||||
| 
 | ||||
|     fun sendMedia(recipients: Set<RankedRecipient>, mediaId: String, scope: CoroutineScope) { | ||||
|         val resultsCount = intArrayOf(0) | ||||
| @ -134,7 +131,10 @@ object DirectMessagesManager { | ||||
|         data.postValue(loading(null)) | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 service.broadcastMediaShare( | ||||
|                 DirectMessagesService.broadcastMediaShare( | ||||
|                     csrfToken, | ||||
|                     viewerId, | ||||
|                     deviceUuid, | ||||
|                     UUID.randomUUID().toString(), | ||||
|                     of(threadId), | ||||
|                     mediaId | ||||
| @ -157,6 +157,5 @@ object DirectMessagesManager { | ||||
|         val csrfToken = getCsrfTokenFromCookie(cookie) | ||||
|         require(!csrfToken.isNullOrBlank() && viewerId != 0L && deviceUuid.isNotBlank()) { "User is not logged in!" } | ||||
|         this.csrfToken = csrfToken | ||||
|         service = getInstance(csrfToken, viewerId, deviceUuid) | ||||
|     } | ||||
| } | ||||
| @ -12,8 +12,8 @@ import awais.instagrabber.models.Resource.Companion.success | ||||
| import awais.instagrabber.repositories.responses.User | ||||
| import awais.instagrabber.repositories.responses.directmessages.* | ||||
| import awais.instagrabber.utils.* | ||||
| import awais.instagrabber.utils.extensions.TAG | ||||
| import awais.instagrabber.webservices.DirectMessagesService | ||||
| import awais.instagrabber.webservices.DirectMessagesService.Companion.getInstance | ||||
| import com.google.common.cache.CacheBuilder | ||||
| import com.google.common.cache.CacheLoader | ||||
| import com.google.common.collect.ImmutableList | ||||
| @ -24,14 +24,13 @@ import retrofit2.Call | ||||
| import java.util.* | ||||
| import java.util.concurrent.TimeUnit | ||||
| 
 | ||||
| class InboxManager private constructor(private val pending: Boolean) { | ||||
| class InboxManager(private val pending: Boolean) { | ||||
|     // private val fetchInboxControlledRunner: ControlledRunner<Resource<DirectInbox>> = ControlledRunner() | ||||
|     // private val fetchPendingInboxControlledRunner: ControlledRunner<Resource<DirectInbox>> = ControlledRunner() | ||||
|     private val inbox = MutableLiveData<Resource<DirectInbox?>>(success(null)) | ||||
|     private val unseenCount = MutableLiveData<Resource<Int?>>() | ||||
|     private val pendingRequestsTotal = MutableLiveData(0) | ||||
|     val threads: LiveData<List<DirectThread>> | ||||
|     private val service: DirectMessagesService | ||||
|     private var inboxRequest: Call<DirectInboxResponse?>? = null | ||||
|     private var unseenCountRequest: Call<DirectBadgeCount?>? = null | ||||
|     private var seqId: Long = 0 | ||||
| @ -58,7 +57,11 @@ class InboxManager private constructor(private val pending: Boolean) { | ||||
|         inbox.postValue(loading(currentDirectInbox)) | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val inboxValue = if (pending) service.fetchPendingInbox(cursor, seqId) else service.fetchInbox(cursor, seqId) | ||||
|                 val inboxValue = if (pending) { | ||||
|                     DirectMessagesService.fetchPendingInbox(cursor, seqId) | ||||
|                 } else { | ||||
|                     DirectMessagesService.fetchInbox(cursor, seqId) | ||||
|                 } | ||||
|                 parseInboxResponse(inboxValue) | ||||
|             } catch (e: Exception) { | ||||
|                 inbox.postValue(error(e.message, currentDirectInbox)) | ||||
| @ -74,7 +77,7 @@ class InboxManager private constructor(private val pending: Boolean) { | ||||
|         unseenCount.postValue(loading(currentUnseenCount)) | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val directBadgeCount = service.fetchUnseenCount() | ||||
|                 val directBadgeCount = DirectMessagesService.fetchUnseenCount() | ||||
|                 unseenCount.postValue(success(directBadgeCount.badgeCount)) | ||||
|             } catch (e: Exception) { | ||||
|                 Log.e(TAG, "Failed fetching unseen count", e) | ||||
| @ -286,7 +289,6 @@ class InboxManager private constructor(private val pending: Boolean) { | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private val TAG = InboxManager::class.java.simpleName | ||||
|         private val THREAD_LOCKS = CacheBuilder | ||||
|             .newBuilder() | ||||
|             .expireAfterAccess(1, TimeUnit.MINUTES) // max lock time ever expected | ||||
| @ -299,10 +301,6 @@ class InboxManager private constructor(private val pending: Boolean) { | ||||
|             if (t2FirstDirectItem == null) return@Comparator -1 | ||||
|             t2FirstDirectItem.getTimestamp().compareTo(t1FirstDirectItem.getTimestamp()) | ||||
|         } | ||||
| 
 | ||||
|         fun getInstance(pending: Boolean): InboxManager { | ||||
|             return InboxManager(pending) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     init { | ||||
| @ -311,7 +309,6 @@ class InboxManager private constructor(private val pending: Boolean) { | ||||
|         val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID) | ||||
|         val csrfToken = getCsrfTokenFromCookie(cookie) | ||||
|         require(!csrfToken.isNullOrBlank() && viewerId != 0L && deviceUuid.isNotBlank()) { "User is not logged in!" } | ||||
|         service = getInstance(csrfToken, viewerId, deviceUuid) | ||||
| 
 | ||||
|         // Transformations | ||||
|         threads = Transformations.distinctUntilChanged(Transformations.map(inbox) { inboxResource: Resource<DirectInbox?> -> | ||||
|  | ||||
| @ -19,8 +19,6 @@ import awais.instagrabber.repositories.requests.UploadFinishOptions | ||||
| import awais.instagrabber.repositories.requests.VideoOptions | ||||
| import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds | ||||
| import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds.Companion.of | ||||
| import awais.instagrabber.repositories.responses.FriendshipChangeResponse | ||||
| import awais.instagrabber.repositories.responses.FriendshipRestrictResponse | ||||
| import awais.instagrabber.repositories.responses.User | ||||
| import awais.instagrabber.repositories.responses.directmessages.* | ||||
| import awais.instagrabber.repositories.responses.giphy.GiphyGif | ||||
| @ -31,10 +29,10 @@ import awais.instagrabber.utils.MediaUploader.uploadVideo | ||||
| import awais.instagrabber.utils.MediaUtils.OnInfoLoadListener | ||||
| import awais.instagrabber.utils.MediaUtils.VideoInfo | ||||
| import awais.instagrabber.utils.TextUtils.isEmpty | ||||
| import awais.instagrabber.utils.extensions.TAG | ||||
| import awais.instagrabber.webservices.DirectMessagesService | ||||
| import awais.instagrabber.webservices.FriendshipService | ||||
| import awais.instagrabber.webservices.MediaService | ||||
| import awais.instagrabber.webservices.ServiceCallback | ||||
| import com.google.common.collect.ImmutableList | ||||
| import com.google.common.collect.Iterables | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| @ -45,17 +43,16 @@ import java.io.File | ||||
| import java.io.IOException | ||||
| import java.net.HttpURLConnection | ||||
| import java.util.* | ||||
| import java.util.concurrent.ConcurrentHashMap | ||||
| import java.util.stream.Collectors | ||||
| 
 | ||||
| class ThreadManager private constructor( | ||||
| class ThreadManager( | ||||
|     private val threadId: String, | ||||
|     pending: Boolean, | ||||
|     currentUser: User, | ||||
|     contentResolver: ContentResolver, | ||||
|     viewerId: Long, | ||||
|     csrfToken: String, | ||||
|     deviceUuid: String, | ||||
|     private val currentUser: User?, | ||||
|     private val contentResolver: ContentResolver, | ||||
|     private val viewerId: Long, | ||||
|     private val csrfToken: String, | ||||
|     private val deviceUuid: String, | ||||
| ) { | ||||
|     private val _fetching = MutableLiveData<Resource<Any?>>() | ||||
|     val fetching: LiveData<Resource<Any?>> = _fetching | ||||
| @ -64,13 +61,7 @@ class ThreadManager private constructor( | ||||
|     private val _pendingRequests = MutableLiveData<DirectThreadParticipantRequestsResponse?>(null) | ||||
|     val pendingRequests: LiveData<DirectThreadParticipantRequestsResponse?> = _pendingRequests | ||||
|     private val inboxManager: InboxManager = if (pending) DirectMessagesManager.pendingInboxManager else DirectMessagesManager.inboxManager | ||||
|     private val viewerId: Long | ||||
|     private val threadIdOrUserIds: ThreadIdOrUserIds = of(threadId) | ||||
|     private val currentUser: User? | ||||
|     private val contentResolver: ContentResolver | ||||
|     private val service: DirectMessagesService | ||||
|     private val mediaService: MediaService | ||||
|     private val friendshipService: FriendshipService | ||||
| 
 | ||||
|     val thread: LiveData<DirectThread?> by lazy { | ||||
|         distinctUntilChanged(map(inboxManager.getInbox()) { inboxResource: Resource<DirectInbox?>? -> | ||||
| @ -135,7 +126,7 @@ class ThreadManager private constructor( | ||||
|         _fetching.postValue(loading(null)) | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val threadFeedResponse = service.fetchThread(threadId, cursor) | ||||
|                 val threadFeedResponse = DirectMessagesService.fetchThread(threadId, cursor) | ||||
|                 if (threadFeedResponse.status != null && threadFeedResponse.status != "ok") { | ||||
|                     _fetching.postValue(error(R.string.generic_not_ok_response, null)) | ||||
|                     return@launch | ||||
| @ -163,7 +154,7 @@ class ThreadManager private constructor( | ||||
|         if (isGroup == null || !isGroup) return | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val response = service.participantRequests(threadId, 1) | ||||
|                 val response = DirectMessagesService.participantRequests(threadId, 1) | ||||
|                 _pendingRequests.postValue(response) | ||||
|             } catch (e: Exception) { | ||||
|                 Log.e(TAG, "fetchPendingRequests: ", e) | ||||
| @ -355,7 +346,10 @@ class ThreadManager private constructor( | ||||
|         val repliedToClientContext = replyToItemValue?.clientContext | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val response = service.broadcastText( | ||||
|                 val response = DirectMessagesService.broadcastText( | ||||
|                     csrfToken, | ||||
|                     viewerId, | ||||
|                     deviceUuid, | ||||
|                     clientContext, | ||||
|                     threadIdOrUserIds, | ||||
|                     text, | ||||
| @ -410,7 +404,10 @@ class ThreadManager private constructor( | ||||
|         data.postValue(loading(directItem)) | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val request = service.broadcastAnimatedMedia( | ||||
|                 val request = DirectMessagesService.broadcastAnimatedMedia( | ||||
|                     csrfToken, | ||||
|                     userId, | ||||
|                     deviceUuid, | ||||
|                     clientContext, | ||||
|                     threadIdOrUserIds, | ||||
|                     giphyGif | ||||
| @ -455,8 +452,11 @@ class ThreadManager private constructor( | ||||
|                     "4", | ||||
|                     null | ||||
|                 ) | ||||
|                 mediaService.uploadFinish(uploadFinishOptions) | ||||
|                 val broadcastResponse = service.broadcastVoice( | ||||
|                 MediaService.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions) | ||||
|                 val broadcastResponse = DirectMessagesService.broadcastVoice( | ||||
|                     csrfToken, | ||||
|                     viewerId, | ||||
|                     deviceUuid, | ||||
|                     clientContext, | ||||
|                     threadIdOrUserIds, | ||||
|                     uploadDmVoiceOptions.uploadId, | ||||
| @ -497,7 +497,10 @@ class ThreadManager private constructor( | ||||
|         } | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 service.broadcastReaction( | ||||
|                 DirectMessagesService.broadcastReaction( | ||||
|                     csrfToken, | ||||
|                     userId, | ||||
|                     deviceUuid, | ||||
|                     clientContext, | ||||
|                     threadIdOrUserIds, | ||||
|                     itemId, | ||||
| @ -534,7 +537,16 @@ class ThreadManager private constructor( | ||||
|         } | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 service.broadcastReaction(clientContext, threadIdOrUserIds, itemId1, null, true) | ||||
|                 DirectMessagesService.broadcastReaction( | ||||
|                     csrfToken, | ||||
|                     viewerId, | ||||
|                     deviceUuid, | ||||
|                     clientContext, | ||||
|                     threadIdOrUserIds, | ||||
|                     itemId1, | ||||
|                     null, | ||||
|                     true | ||||
|                 ) | ||||
|             } catch (e: Exception) { | ||||
|                 data.postValue(error(e.message, null)) | ||||
|                 Log.e(TAG, "sendDeleteReaction: ", e) | ||||
| @ -553,7 +565,7 @@ class ThreadManager private constructor( | ||||
|         } | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 service.deleteItem(threadId, itemId) | ||||
|                 DirectMessagesService.deleteItem(csrfToken, deviceUuid, threadId, itemId) | ||||
|             } catch (e: Exception) { | ||||
|                 // add the item back if unsuccessful | ||||
|                 addItems(index, listOf(item)) | ||||
| @ -629,7 +641,7 @@ class ThreadManager private constructor( | ||||
|         } | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 service.forward( | ||||
|                 DirectMessagesService.forward( | ||||
|                     thread.threadId, | ||||
|                     itemTypeName, | ||||
|                     threadId, | ||||
| @ -648,7 +660,7 @@ class ThreadManager private constructor( | ||||
|         val data = MutableLiveData<Resource<Any?>>() | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 service.approveRequest(threadId) | ||||
|                 DirectMessagesService.approveRequest(csrfToken, deviceUuid, threadId) | ||||
|                 data.postValue(success(Any())) | ||||
|             } catch (e: Exception) { | ||||
|                 Log.e(TAG, "acceptRequest: ", e) | ||||
| @ -662,7 +674,7 @@ class ThreadManager private constructor( | ||||
|         val data = MutableLiveData<Resource<Any?>>() | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 service.declineRequest(threadId) | ||||
|                 DirectMessagesService.declineRequest(csrfToken, deviceUuid, threadId) | ||||
|                 data.postValue(success(Any())) | ||||
|             } catch (e: Exception) { | ||||
|                 Log.e(TAG, "declineRequest: ", e) | ||||
| @ -707,9 +719,8 @@ class ThreadManager private constructor( | ||||
|         height: Int, | ||||
|         scope: CoroutineScope, | ||||
|     ) { | ||||
|         val userId = getCurrentUserId(data) ?: return | ||||
|         val clientContext = UUID.randomUUID().toString() | ||||
|         val directItem = createImageOrVideo(userId, clientContext, uri, width, height, false) | ||||
|         val directItem = createImageOrVideo(viewerId, clientContext, uri, width, height, false) | ||||
|         directItem.isPending = true | ||||
|         addItems(0, listOf(directItem)) | ||||
|         data.postValue(loading(directItem)) | ||||
| @ -719,7 +730,7 @@ class ThreadManager private constructor( | ||||
|                 if (handleInvalidResponse(data, response)) return@launch | ||||
|                 val response1 = response.response ?: return@launch | ||||
|                 val uploadId = response1.optString("upload_id") | ||||
|                 val response2 = service.broadcastPhoto(clientContext, threadIdOrUserIds, uploadId) | ||||
|                 val response2 = DirectMessagesService.broadcastPhoto(csrfToken, viewerId, deviceUuid, clientContext, threadIdOrUserIds, uploadId) | ||||
|                 parseResponse(response2, data, directItem) | ||||
|             } catch (e: Exception) { | ||||
|                 data.postValue(error(e.message, null)) | ||||
| @ -779,8 +790,11 @@ class ThreadManager private constructor( | ||||
|                     "2", | ||||
|                     VideoOptions(duration / 1000f, emptyList(), 0, false) | ||||
|                 ) | ||||
|                 mediaService.uploadFinish(uploadFinishOptions) | ||||
|                 val broadcastResponse = service.broadcastVideo( | ||||
|                 MediaService.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions) | ||||
|                 val broadcastResponse = DirectMessagesService.broadcastVideo( | ||||
|                     csrfToken, | ||||
|                     viewerId, | ||||
|                     deviceUuid, | ||||
|                     clientContext, | ||||
|                     threadIdOrUserIds, | ||||
|                     uploadDmVideoOptions.uploadId, | ||||
| @ -907,7 +921,7 @@ class ThreadManager private constructor( | ||||
|         val data = MutableLiveData<Resource<Any?>>() | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val response = service.updateTitle(threadId, newTitle.trim()) | ||||
|                 val response = DirectMessagesService.updateTitle(csrfToken, deviceUuid, threadId, newTitle.trim()) | ||||
|                 handleDetailsChangeResponse(data, response) | ||||
|             } catch (e: Exception) { | ||||
|             } | ||||
| @ -919,7 +933,9 @@ class ThreadManager private constructor( | ||||
|         val data = MutableLiveData<Resource<Any?>>() | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val response = service.addUsers( | ||||
|                 val response = DirectMessagesService.addUsers( | ||||
|                     csrfToken, | ||||
|                     deviceUuid, | ||||
|                     threadId, | ||||
|                     users.map { obj: User -> obj.pk } | ||||
|                 ) | ||||
| @ -936,7 +952,7 @@ class ThreadManager private constructor( | ||||
|         val data = MutableLiveData<Resource<Any?>>() | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 service.removeUsers(threadId, setOf(user.pk)) | ||||
|                 DirectMessagesService.removeUsers(csrfToken, deviceUuid, threadId, setOf(user.pk)) | ||||
|                 data.postValue(success(Any())) | ||||
|                 var activeUsers = users.value | ||||
|                 var leftUsersValue = leftUsers.value | ||||
| @ -971,7 +987,7 @@ class ThreadManager private constructor( | ||||
|         if (isAdmin(user)) return data | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 service.addAdmins(threadId, setOf(user.pk)) | ||||
|                 DirectMessagesService.addAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk)) | ||||
|                 val currentAdminIds = adminUserIds.value | ||||
|                 val updatedAdminIds = ImmutableList.builder<Long>() | ||||
|                     .addAll(currentAdminIds ?: emptyList()) | ||||
| @ -999,7 +1015,7 @@ class ThreadManager private constructor( | ||||
|         if (!isAdmin(user)) return data | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 service.removeAdmins(threadId, setOf(user.pk)) | ||||
|                 DirectMessagesService.removeAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk)) | ||||
|                 val currentAdmins = adminUserIds.value ?: return@launch | ||||
|                 val updatedAdminUserIds = currentAdmins.filter { userId1: Long -> userId1 != user.pk } | ||||
|                 val currentThread = thread.value ?: return@launch | ||||
| @ -1029,7 +1045,7 @@ class ThreadManager private constructor( | ||||
|         } | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 service.mute(threadId) | ||||
|                 DirectMessagesService.mute(csrfToken, deviceUuid, threadId) | ||||
|                 data.postValue(success(Any())) | ||||
|                 val currentThread = thread.value ?: return@launch | ||||
|                 try { | ||||
| @ -1057,7 +1073,7 @@ class ThreadManager private constructor( | ||||
|         } | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 service.unmute(threadId) | ||||
|                 DirectMessagesService.unmute(csrfToken, deviceUuid, threadId) | ||||
|                 data.postValue(success(Any())) | ||||
|                 val currentThread = thread.value ?: return@launch | ||||
|                 try { | ||||
| @ -1085,7 +1101,7 @@ class ThreadManager private constructor( | ||||
|         } | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 service.muteMentions(threadId) | ||||
|                 DirectMessagesService.muteMentions(csrfToken, deviceUuid, threadId) | ||||
|                 data.postValue(success(Any())) | ||||
|                 val currentThread = thread.value ?: return@launch | ||||
|                 try { | ||||
| @ -1113,7 +1129,7 @@ class ThreadManager private constructor( | ||||
|         } | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 service.unmuteMentions(threadId) | ||||
|                 DirectMessagesService.unmuteMentions(csrfToken, deviceUuid, threadId) | ||||
|                 data.postValue(success(Any())) | ||||
|                 val currentThread = thread.value ?: return@launch | ||||
|                 try { | ||||
| @ -1133,61 +1149,57 @@ class ThreadManager private constructor( | ||||
| 
 | ||||
|     fun blockUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> { | ||||
|         val data = MutableLiveData<Resource<Any?>>() | ||||
|         friendshipService.changeBlock(false, user.pk, object : ServiceCallback<FriendshipChangeResponse?> { | ||||
|             override fun onSuccess(result: FriendshipChangeResponse?) { | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 FriendshipService.changeBlock(csrfToken, viewerId, deviceUuid, false, user.pk) | ||||
|                 refreshChats(scope) | ||||
|             } catch (e: Exception) { | ||||
|                 Log.e(TAG, "onFailure: ", e) | ||||
|                 data.postValue(error(e.message, null)) | ||||
|             } | ||||
| 
 | ||||
|             override fun onFailure(t: Throwable) { | ||||
|                 Log.e(TAG, "onFailure: ", t) | ||||
|                 data.postValue(error(t.message, null)) | ||||
|             } | ||||
|         }) | ||||
|         } | ||||
|         return data | ||||
|     } | ||||
| 
 | ||||
|     fun unblockUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> { | ||||
|         val data = MutableLiveData<Resource<Any?>>() | ||||
|         friendshipService.changeBlock(true, user.pk, object : ServiceCallback<FriendshipChangeResponse?> { | ||||
|             override fun onSuccess(result: FriendshipChangeResponse?) { | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 FriendshipService.changeBlock(csrfToken, viewerId, deviceUuid, true, user.pk) | ||||
|                 refreshChats(scope) | ||||
|             } catch (e: Exception) { | ||||
|                 Log.e(TAG, "onFailure: ", e) | ||||
|                 data.postValue(error(e.message, null)) | ||||
|             } | ||||
| 
 | ||||
|             override fun onFailure(t: Throwable) { | ||||
|                 Log.e(TAG, "onFailure: ", t) | ||||
|                 data.postValue(error(t.message, null)) | ||||
|             } | ||||
|         }) | ||||
|         } | ||||
|         return data | ||||
|     } | ||||
| 
 | ||||
|     fun restrictUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> { | ||||
|         val data = MutableLiveData<Resource<Any?>>() | ||||
|         friendshipService.toggleRestrict(user.pk, true, object : ServiceCallback<FriendshipRestrictResponse?> { | ||||
|             override fun onSuccess(result: FriendshipRestrictResponse?) { | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 FriendshipService.toggleRestrict(csrfToken, deviceUuid, user.pk, true) | ||||
|                 refreshChats(scope) | ||||
|             } catch (e: Exception) { | ||||
|                 Log.e(TAG, "onFailure: ", e) | ||||
|                 data.postValue(error(e.message, null)) | ||||
|             } | ||||
| 
 | ||||
|             override fun onFailure(t: Throwable) { | ||||
|                 Log.e(TAG, "onFailure: ", t) | ||||
|                 data.postValue(error(t.message, null)) | ||||
|             } | ||||
|         }) | ||||
|         } | ||||
|         return data | ||||
|     } | ||||
| 
 | ||||
|     fun unRestrictUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> { | ||||
|         val data = MutableLiveData<Resource<Any?>>() | ||||
|         friendshipService.toggleRestrict(user.pk, false, object : ServiceCallback<FriendshipRestrictResponse?> { | ||||
|             override fun onSuccess(result: FriendshipRestrictResponse?) { | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 FriendshipService.toggleRestrict(csrfToken, deviceUuid, user.pk, false) | ||||
|                 refreshChats(scope) | ||||
|             } catch (e: Exception) { | ||||
|                 Log.e(TAG, "onFailure: ", e) | ||||
|                 data.postValue(error(e.message, null)) | ||||
|             } | ||||
| 
 | ||||
|             override fun onFailure(t: Throwable) { | ||||
|                 Log.e(TAG, "onFailure: ", t) | ||||
|                 data.postValue(error(t.message, null)) | ||||
|             } | ||||
|         }) | ||||
|         } | ||||
|         return data | ||||
|     } | ||||
| 
 | ||||
| @ -1196,7 +1208,9 @@ class ThreadManager private constructor( | ||||
|         data.postValue(loading(null)) | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val response = service.approveParticipantRequests( | ||||
|                 val response = DirectMessagesService.approveParticipantRequests( | ||||
|                     csrfToken, | ||||
|                     deviceUuid, | ||||
|                     threadId, | ||||
|                     users.map { obj: User -> obj.pk } | ||||
|                 ) | ||||
| @ -1215,7 +1229,9 @@ class ThreadManager private constructor( | ||||
|         data.postValue(loading(null)) | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val response = service.declineParticipantRequests( | ||||
|                 val response = DirectMessagesService.declineParticipantRequests( | ||||
|                     csrfToken, | ||||
|                     deviceUuid, | ||||
|                     threadId, | ||||
|                     users.map { obj: User -> obj.pk } | ||||
|                 ) | ||||
| @ -1255,7 +1271,7 @@ class ThreadManager private constructor( | ||||
|         } | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val response = service.approvalRequired(threadId) | ||||
|                 val response = DirectMessagesService.approvalRequired(csrfToken, deviceUuid, threadId) | ||||
|                 handleDetailsChangeResponse(data, response) | ||||
|                 val currentThread = thread.value ?: return@launch | ||||
|                 try { | ||||
| @ -1283,7 +1299,7 @@ class ThreadManager private constructor( | ||||
|         } | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val request = service.approvalNotRequired(threadId) | ||||
|                 val request = DirectMessagesService.approvalNotRequired(csrfToken, deviceUuid, threadId) | ||||
|                 handleDetailsChangeResponse(data, request) | ||||
|                 val currentThread = thread.value ?: return@launch | ||||
|                 try { | ||||
| @ -1306,7 +1322,7 @@ class ThreadManager private constructor( | ||||
|         data.postValue(loading(null)) | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val request = service.leave(threadId) | ||||
|                 val request = DirectMessagesService.leave(csrfToken, deviceUuid, threadId) | ||||
|                 handleDetailsChangeResponse(data, request) | ||||
|             } catch (e: Exception) { | ||||
|                 Log.e(TAG, "leave: ", e) | ||||
| @ -1321,7 +1337,7 @@ class ThreadManager private constructor( | ||||
|         data.postValue(loading(null)) | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val request = service.end(threadId) | ||||
|                 val request = DirectMessagesService.end(csrfToken, deviceUuid, threadId) | ||||
|                 handleDetailsChangeResponse(data, request) | ||||
|                 val currentThread = thread.value ?: return@launch | ||||
|                 try { | ||||
| @ -1358,7 +1374,7 @@ class ThreadManager private constructor( | ||||
|         data.postValue(loading(null)) | ||||
|         scope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val response = service.markAsSeen(threadId, directItem) | ||||
|                 val response = DirectMessagesService.markAsSeen(csrfToken, deviceUuid, threadId, directItem) | ||||
|                 if (response == null) { | ||||
|                     data.postValue(error(R.string.generic_null_response, null)) | ||||
|                     return@launch | ||||
| @ -1381,43 +1397,4 @@ class ThreadManager private constructor( | ||||
|         } | ||||
|         return data | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private val TAG = ThreadManager::class.java.simpleName | ||||
|         private val LOCK = Any() | ||||
|         private val INSTANCE_MAP: MutableMap<String, ThreadManager> = ConcurrentHashMap() | ||||
| 
 | ||||
|         @JvmStatic | ||||
|         fun getInstance( | ||||
|             threadId: String, | ||||
|             pending: Boolean, | ||||
|             currentUser: User, | ||||
|             contentResolver: ContentResolver, | ||||
|             viewerId: Long, | ||||
|             csrfToken: String, | ||||
|             deviceUuid: String, | ||||
|         ): ThreadManager { | ||||
|             var instance = INSTANCE_MAP[threadId] | ||||
|             if (instance == null) { | ||||
|                 synchronized(LOCK) { | ||||
|                     instance = INSTANCE_MAP[threadId] | ||||
|                     if (instance == null) { | ||||
|                         instance = ThreadManager(threadId, pending, currentUser, contentResolver, viewerId, csrfToken, deviceUuid) | ||||
|                         INSTANCE_MAP[threadId] = instance!! | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             return instance!! | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     init { | ||||
|         this.currentUser = currentUser | ||||
|         this.contentResolver = contentResolver | ||||
|         this.viewerId = viewerId | ||||
|         service = DirectMessagesService.getInstance(csrfToken, viewerId, deviceUuid) | ||||
|         mediaService = MediaService.getInstance(deviceUuid, csrfToken, viewerId) | ||||
|         friendshipService = FriendshipService.getInstance(deviceUuid, csrfToken, viewerId) | ||||
|         // fetchChats(); | ||||
|     } | ||||
| } | ||||
| @ -1,37 +0,0 @@ | ||||
| package awais.instagrabber.repositories; | ||||
| 
 | ||||
| import java.util.Map; | ||||
| 
 | ||||
| import awais.instagrabber.repositories.responses.FriendshipChangeResponse; | ||||
| import awais.instagrabber.repositories.responses.FriendshipRestrictResponse; | ||||
| import retrofit2.Call; | ||||
| import retrofit2.http.FieldMap; | ||||
| import retrofit2.http.FormUrlEncoded; | ||||
| import retrofit2.http.GET; | ||||
| import retrofit2.http.POST; | ||||
| import retrofit2.http.Path; | ||||
| import retrofit2.http.QueryMap; | ||||
| 
 | ||||
| public interface FriendshipRepository { | ||||
| 
 | ||||
|     @FormUrlEncoded | ||||
|     @POST("/api/v1/friendships/{action}/{id}/") | ||||
|     Call<FriendshipChangeResponse> change(@Path("action") String action, | ||||
|                                           @Path("id") long id, | ||||
|                                           @FieldMap Map<String, String> form); | ||||
| 
 | ||||
|     @FormUrlEncoded | ||||
|     @POST("/api/v1/restrict_action/{action}/") | ||||
|     Call<FriendshipRestrictResponse> toggleRestrict(@Path("action") String action, | ||||
|                                                     @FieldMap Map<String, String> form); | ||||
| 
 | ||||
|     @GET("/api/v1/friendships/{userId}/{type}/") | ||||
|     Call<String> getList(@Path("userId") long userId, | ||||
|                          @Path("type") String type, // following or followers | ||||
|                          @QueryMap(encoded = true) Map<String, String> queryParams); | ||||
| 
 | ||||
|     @FormUrlEncoded | ||||
|     @POST("/api/v1/friendships/{action}/") | ||||
|     Call<FriendshipChangeResponse> changeMute(@Path("action") String action, | ||||
|                                               @FieldMap Map<String, String> form); | ||||
| } | ||||
| @ -0,0 +1,36 @@ | ||||
| package awais.instagrabber.repositories | ||||
| 
 | ||||
| import awais.instagrabber.repositories.responses.FriendshipChangeResponse | ||||
| import awais.instagrabber.repositories.responses.FriendshipRestrictResponse | ||||
| import retrofit2.http.* | ||||
| 
 | ||||
| interface FriendshipRepository { | ||||
|     @FormUrlEncoded | ||||
|     @POST("/api/v1/friendships/{action}/{id}/") | ||||
|     suspend fun change( | ||||
|         @Path("action") action: String, | ||||
|         @Path("id") id: Long, | ||||
|         @FieldMap form: Map<String, String>, | ||||
|     ): FriendshipChangeResponse | ||||
| 
 | ||||
|     @FormUrlEncoded | ||||
|     @POST("/api/v1/restrict_action/{action}/") | ||||
|     suspend fun toggleRestrict( | ||||
|         @Path("action") action: String, | ||||
|         @FieldMap form: Map<String, String>, | ||||
|     ): FriendshipRestrictResponse | ||||
| 
 | ||||
|     @GET("/api/v1/friendships/{userId}/{type}/") | ||||
|     suspend fun getList( | ||||
|         @Path("userId") userId: Long, | ||||
|         @Path("type") type: String,  // following or followers | ||||
|         @QueryMap(encoded = true) queryParams: Map<String, String>, | ||||
|     ): String | ||||
| 
 | ||||
|     @FormUrlEncoded | ||||
|     @POST("/api/v1/friendships/{action}/") | ||||
|     suspend fun changeMute( | ||||
|         @Path("action") action: String, | ||||
|         @FieldMap form: Map<String, String>, | ||||
|     ): FriendshipChangeResponse | ||||
| } | ||||
| @ -1,25 +0,0 @@ | ||||
| package awais.instagrabber.repositories; | ||||
| 
 | ||||
| import java.util.Map; | ||||
| 
 | ||||
| import retrofit2.Call; | ||||
| import retrofit2.http.GET; | ||||
| import retrofit2.http.Path; | ||||
| import retrofit2.http.QueryMap; | ||||
| 
 | ||||
| public interface GraphQLRepository { | ||||
|     @GET("/graphql/query/") | ||||
|     Call<String> fetch(@QueryMap(encoded = true) Map<String, String> queryParams); | ||||
| 
 | ||||
|     @GET("/{username}/?__a=1") | ||||
|     Call<String> getUser(@Path("username") String username); | ||||
| 
 | ||||
|     @GET("/p/{shortcode}/?__a=1") | ||||
|     Call<String> getPost(@Path("shortcode") String shortcode); | ||||
| 
 | ||||
|     @GET("/explore/tags/{tag}/?__a=1") | ||||
|     Call<String> getTag(@Path("tag") String tag); | ||||
| 
 | ||||
|     @GET("/explore/locations/{locationId}/?__a=1") | ||||
|     Call<String> getLocation(@Path("locationId") long locationId); | ||||
| } | ||||
| @ -0,0 +1,22 @@ | ||||
| package awais.instagrabber.repositories | ||||
| 
 | ||||
| import retrofit2.http.GET | ||||
| import retrofit2.http.Path | ||||
| import retrofit2.http.QueryMap | ||||
| 
 | ||||
| interface GraphQLRepository { | ||||
|     @GET("/graphql/query/") | ||||
|     suspend fun fetch(@QueryMap(encoded = true) queryParams: Map<String, String>): String | ||||
| 
 | ||||
|     @GET("/{username}/?__a=1") | ||||
|     suspend fun getUser(@Path("username") username: String): String | ||||
| 
 | ||||
|     @GET("/p/{shortcode}/?__a=1") | ||||
|     suspend fun getPost(@Path("shortcode") shortcode: String): String | ||||
| 
 | ||||
|     @GET("/explore/tags/{tag}/?__a=1") | ||||
|     suspend fun getTag(@Path("tag") tag: String): String | ||||
| 
 | ||||
|     @GET("/explore/locations/{locationId}/?__a=1") | ||||
|     suspend fun getLocation(@Path("locationId") locationId: Long): String | ||||
| } | ||||
| @ -1,43 +0,0 @@ | ||||
| package awais.instagrabber.repositories; | ||||
| 
 | ||||
| import java.util.Map; | ||||
| 
 | ||||
| import awais.instagrabber.repositories.responses.StoryStickerResponse; | ||||
| import retrofit2.Call; | ||||
| import retrofit2.http.FieldMap; | ||||
| import retrofit2.http.FormUrlEncoded; | ||||
| import retrofit2.http.GET; | ||||
| import retrofit2.http.POST; | ||||
| import retrofit2.http.Path; | ||||
| import retrofit2.http.QueryMap; | ||||
| import retrofit2.http.Url; | ||||
| 
 | ||||
| public interface StoriesRepository { | ||||
|     @GET("/api/v1/media/{mediaId}/info/") | ||||
|     Call<String> fetch(@Path("mediaId") final long mediaId); | ||||
|     // this one is the same as MediaRepository.fetch BUT you need to make sure it's a story | ||||
| 
 | ||||
|     @GET("/api/v1/feed/reels_tray/") | ||||
|     Call<String> getFeedStories(); | ||||
| 
 | ||||
|     @GET("/api/v1/highlights/{uid}/highlights_tray/") | ||||
|     Call<String> fetchHighlights(@Path("uid") final long uid); | ||||
| 
 | ||||
|     @GET("/api/v1/archive/reel/day_shells/") | ||||
|     Call<String> fetchArchive(@QueryMap Map<String, String> queryParams); | ||||
| 
 | ||||
|     @GET | ||||
|     Call<String> getUserStory(@Url String url); | ||||
| 
 | ||||
|     @FormUrlEncoded | ||||
|     @POST("/api/v1/media/{storyId}/{stickerId}/{action}/") | ||||
|     Call<StoryStickerResponse> respondToSticker(@Path("storyId") String storyId, | ||||
|                                                 @Path("stickerId") String stickerId, | ||||
|                                                 @Path("action") String action, | ||||
|                                                 // story_poll_vote, story_question_response, story_slider_vote, story_quiz_answer | ||||
|                                                 @FieldMap Map<String, String> form); | ||||
| 
 | ||||
|     @FormUrlEncoded | ||||
|     @POST("/api/v2/media/seen/") | ||||
|     Call<String> seen(@QueryMap Map<String, String> queryParams, @FieldMap Map<String, String> form); | ||||
| } | ||||
| @ -0,0 +1,38 @@ | ||||
| package awais.instagrabber.repositories | ||||
| 
 | ||||
| import awais.instagrabber.repositories.responses.StoryStickerResponse | ||||
| import retrofit2.http.* | ||||
| 
 | ||||
| interface StoriesRepository { | ||||
|     // this one is the same as MediaRepository.fetch BUT you need to make sure it's a story | ||||
|     @GET("/api/v1/media/{mediaId}/info/") | ||||
|     suspend fun fetch(@Path("mediaId") mediaId: Long): String | ||||
| 
 | ||||
|     @GET("/api/v1/feed/reels_tray/") | ||||
|     suspend fun getFeedStories(): String | ||||
| 
 | ||||
|     @GET("/api/v1/highlights/{uid}/highlights_tray/") | ||||
|     suspend fun fetchHighlights(@Path("uid") uid: Long): String | ||||
| 
 | ||||
|     @GET("/api/v1/archive/reel/day_shells/") | ||||
|     suspend fun fetchArchive(@QueryMap queryParams: Map<String, String>): String | ||||
| 
 | ||||
|     @GET | ||||
|     suspend fun getUserStory(@Url url: String): String | ||||
| 
 | ||||
|     @FormUrlEncoded | ||||
|     @POST("/api/v1/media/{storyId}/{stickerId}/{action}/") | ||||
|     suspend fun respondToSticker( | ||||
|         @Path("storyId") storyId: String, | ||||
|         @Path("stickerId") stickerId: String, | ||||
|         @Path("action") action: String,  // story_poll_vote, story_question_response, story_slider_vote, story_quiz_answer | ||||
|         @FieldMap form: Map<String, String>, | ||||
|     ): StoryStickerResponse | ||||
| 
 | ||||
|     @FormUrlEncoded | ||||
|     @POST("/api/v2/media/seen/") | ||||
|     suspend fun seen( | ||||
|         @QueryMap queryParams: Map<String, String>, | ||||
|         @FieldMap form: Map<String, String>, | ||||
|     ): String | ||||
| } | ||||
| @ -1,25 +0,0 @@ | ||||
| package awais.instagrabber.repositories; | ||||
| 
 | ||||
| import awais.instagrabber.repositories.responses.FriendshipStatus; | ||||
| import awais.instagrabber.repositories.responses.UserSearchResponse; | ||||
| import awais.instagrabber.repositories.responses.WrappedUser; | ||||
| import retrofit2.Call; | ||||
| import retrofit2.http.GET; | ||||
| import retrofit2.http.Path; | ||||
| import retrofit2.http.Query; | ||||
| 
 | ||||
| public interface UserRepository { | ||||
| 
 | ||||
|     @GET("/api/v1/users/{uid}/info/") | ||||
|     Call<WrappedUser> getUserInfo(@Path("uid") final long uid); | ||||
| 
 | ||||
|     @GET("/api/v1/users/{username}/usernameinfo/") | ||||
|     Call<WrappedUser> getUsernameInfo(@Path("username") final String username); | ||||
| 
 | ||||
|     @GET("/api/v1/friendships/show/{uid}/") | ||||
|     Call<FriendshipStatus> getUserFriendship(@Path("uid") final long uid); | ||||
| 
 | ||||
|     @GET("/api/v1/users/search/") | ||||
|     Call<UserSearchResponse> search(@Query("timezone_offset") float timezoneOffset, | ||||
|                                     @Query("q") String query); | ||||
| } | ||||
| @ -0,0 +1,25 @@ | ||||
| package awais.instagrabber.repositories | ||||
| 
 | ||||
| import awais.instagrabber.repositories.responses.FriendshipStatus | ||||
| import awais.instagrabber.repositories.responses.UserSearchResponse | ||||
| import awais.instagrabber.repositories.responses.WrappedUser | ||||
| import retrofit2.http.GET | ||||
| import retrofit2.http.Path | ||||
| import retrofit2.http.Query | ||||
| 
 | ||||
| interface UserRepository { | ||||
|     @GET("/api/v1/users/{uid}/info/") | ||||
|     suspend fun getUserInfo(@Path("uid") uid: Long): WrappedUser | ||||
| 
 | ||||
|     @GET("/api/v1/users/{username}/usernameinfo/") | ||||
|     suspend fun getUsernameInfo(@Path("username") username: String): WrappedUser | ||||
| 
 | ||||
|     @GET("/api/v1/friendships/show/{uid}/") | ||||
|     suspend fun getUserFriendship(@Path("uid") uid: Long): FriendshipStatus | ||||
| 
 | ||||
|     @GET("/api/v1/users/search/") | ||||
|     suspend fun search( | ||||
|         @Query("timezone_offset") timezoneOffset: Float, | ||||
|         @Query("q") query: String, | ||||
|     ): UserSearchResponse | ||||
| } | ||||
| @ -1,296 +0,0 @@ | ||||
| package awais.instagrabber.repositories.responses; | ||||
| 
 | ||||
| import java.io.Serializable; | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
| 
 | ||||
| public class User implements Serializable { | ||||
|     private final long pk; | ||||
|     private final String username; | ||||
|     private final String fullName; | ||||
|     private final boolean isPrivate; | ||||
|     private final String profilePicUrl; | ||||
|     private final String profilePicId; | ||||
|     private FriendshipStatus friendshipStatus; | ||||
|     private final boolean isVerified; | ||||
|     private final boolean hasAnonymousProfilePicture; | ||||
|     private final boolean isUnpublished; | ||||
|     private final boolean isFavorite; | ||||
|     private final boolean isDirectappInstalled; | ||||
|     private final boolean hasChaining; | ||||
|     private final String reelAutoArchive; | ||||
|     private final String allowedCommenterType; | ||||
|     private final long mediaCount; | ||||
|     private final long followerCount; | ||||
|     private final long followingCount; | ||||
|     private final long followingTagCount; | ||||
|     private final String biography; | ||||
|     private final String externalUrl; | ||||
|     private final long usertagsCount; | ||||
|     private final String publicEmail; | ||||
|     private final HdProfilePicUrlInfo hdProfilePicUrlInfo; | ||||
|     private final String profileContext; // "also followed by" your friends | ||||
|     private final List<UserProfileContextLink> profileContextLinksWithUserIds; // ^ | ||||
|     private final String socialContext; // AYML | ||||
|     private final String interopMessagingUserFbid; // in DMs only: Facebook user ID | ||||
| 
 | ||||
|     public User(final long pk, | ||||
|                 final String username, | ||||
|                 final String fullName, | ||||
|                 final boolean isPrivate, | ||||
|                 final String profilePicUrl, | ||||
|                 final String profilePicId, | ||||
|                 final FriendshipStatus friendshipStatus, | ||||
|                 final boolean isVerified, | ||||
|                 final boolean hasAnonymousProfilePicture, | ||||
|                 final boolean isUnpublished, | ||||
|                 final boolean isFavorite, | ||||
|                 final boolean isDirectappInstalled, | ||||
|                 final boolean hasChaining, | ||||
|                 final String reelAutoArchive, | ||||
|                 final String allowedCommenterType, | ||||
|                 final long mediaCount, | ||||
|                 final long followerCount, | ||||
|                 final long followingCount, | ||||
|                 final long followingTagCount, | ||||
|                 final String biography, | ||||
|                 final String externalUrl, | ||||
|                 final long usertagsCount, | ||||
|                 final String publicEmail, | ||||
|                 final HdProfilePicUrlInfo hdProfilePicUrlInfo, | ||||
|                 final String profileContext, | ||||
|                 final List<UserProfileContextLink> profileContextLinksWithUserIds, | ||||
|                 final String socialContext, | ||||
|                 final String interopMessagingUserFbid) { | ||||
|         this.pk = pk; | ||||
|         this.username = username; | ||||
|         this.fullName = fullName; | ||||
|         this.isPrivate = isPrivate; | ||||
|         this.profilePicUrl = profilePicUrl; | ||||
|         this.profilePicId = profilePicId; | ||||
|         this.friendshipStatus = friendshipStatus; | ||||
|         this.isVerified = isVerified; | ||||
|         this.hasAnonymousProfilePicture = hasAnonymousProfilePicture; | ||||
|         this.isUnpublished = isUnpublished; | ||||
|         this.isFavorite = isFavorite; | ||||
|         this.isDirectappInstalled = isDirectappInstalled; | ||||
|         this.hasChaining = hasChaining; | ||||
|         this.reelAutoArchive = reelAutoArchive; | ||||
|         this.allowedCommenterType = allowedCommenterType; | ||||
|         this.mediaCount = mediaCount; | ||||
|         this.followerCount = followerCount; | ||||
|         this.followingCount = followingCount; | ||||
|         this.followingTagCount = followingTagCount; | ||||
|         this.biography = biography; | ||||
|         this.externalUrl = externalUrl; | ||||
|         this.usertagsCount = usertagsCount; | ||||
|         this.publicEmail = publicEmail; | ||||
|         this.hdProfilePicUrlInfo = hdProfilePicUrlInfo; | ||||
|         this.profileContext = profileContext; | ||||
|         this.profileContextLinksWithUserIds = profileContextLinksWithUserIds; | ||||
|         this.socialContext = socialContext; | ||||
|         this.interopMessagingUserFbid = interopMessagingUserFbid; | ||||
|     } | ||||
| 
 | ||||
|     public User(final long pk, | ||||
|                 final String username, | ||||
|                 final String fullName, | ||||
|                 final boolean isPrivate, | ||||
|                 final String profilePicUrl, | ||||
|                 final boolean isVerified) { | ||||
|         this.pk = pk; | ||||
|         this.username = username; | ||||
|         this.fullName = fullName; | ||||
|         this.isPrivate = isPrivate; | ||||
|         this.profilePicUrl = profilePicUrl; | ||||
|         this.profilePicId = null; | ||||
|         this.friendshipStatus = new FriendshipStatus( | ||||
|                 false, | ||||
|                 false, | ||||
|                 false, | ||||
|                 false, | ||||
|                 false, | ||||
|                 false, | ||||
|                 false, | ||||
|                 false, | ||||
|                 false, | ||||
|                 false | ||||
|         ); | ||||
|         this.isVerified = isVerified; | ||||
|         this.hasAnonymousProfilePicture = false; | ||||
|         this.isUnpublished = false; | ||||
|         this.isFavorite = false; | ||||
|         this.isDirectappInstalled = false; | ||||
|         this.hasChaining = false; | ||||
|         this.reelAutoArchive = null; | ||||
|         this.allowedCommenterType = null; | ||||
|         this.mediaCount = 0; | ||||
|         this.followerCount = 0; | ||||
|         this.followingCount = 0; | ||||
|         this.followingTagCount = 0; | ||||
|         this.biography = null; | ||||
|         this.externalUrl = null; | ||||
|         this.usertagsCount = 0; | ||||
|         this.publicEmail = null; | ||||
|         this.hdProfilePicUrlInfo = null; | ||||
|         this.profileContext = null; | ||||
|         this.profileContextLinksWithUserIds = null; | ||||
|         this.socialContext = null; | ||||
|         this.interopMessagingUserFbid = null; | ||||
|     } | ||||
| 
 | ||||
|     public long getPk() { | ||||
|         return pk; | ||||
|     } | ||||
| 
 | ||||
|     public String getUsername() { | ||||
|         return username; | ||||
|     } | ||||
| 
 | ||||
|     public String getFullName() { | ||||
|         return fullName; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isPrivate() { | ||||
|         return isPrivate; | ||||
|     } | ||||
| 
 | ||||
|     public String getProfilePicUrl() { | ||||
|         return profilePicUrl; | ||||
|     } | ||||
| 
 | ||||
|     public String getHDProfilePicUrl() { | ||||
|         if (hdProfilePicUrlInfo == null) { | ||||
|             return getProfilePicUrl(); | ||||
|         } | ||||
|         return hdProfilePicUrlInfo.getUrl(); | ||||
|     } | ||||
| 
 | ||||
|     public String getProfilePicId() { | ||||
|         return profilePicId; | ||||
|     } | ||||
| 
 | ||||
|     public FriendshipStatus getFriendshipStatus() { | ||||
|         return friendshipStatus; | ||||
|     } | ||||
| 
 | ||||
|     public void setFriendshipStatus(final FriendshipStatus friendshipStatus) { | ||||
|         this.friendshipStatus = friendshipStatus; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isVerified() { | ||||
|         return isVerified; | ||||
|     } | ||||
| 
 | ||||
|     public boolean hasAnonymousProfilePicture() { | ||||
|         return hasAnonymousProfilePicture; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isUnpublished() { | ||||
|         return isUnpublished; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isFavorite() { | ||||
|         return isFavorite; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isDirectappInstalled() { | ||||
|         return isDirectappInstalled; | ||||
|     } | ||||
| 
 | ||||
|     public boolean hasChaining() { | ||||
|         return hasChaining; | ||||
|     } | ||||
| 
 | ||||
|     public String getReelAutoArchive() { | ||||
|         return reelAutoArchive; | ||||
|     } | ||||
| 
 | ||||
|     public String getAllowedCommenterType() { | ||||
|         return allowedCommenterType; | ||||
|     } | ||||
| 
 | ||||
|     public long getMediaCount() { | ||||
|         return mediaCount; | ||||
|     } | ||||
| 
 | ||||
|     public long getFollowerCount() { | ||||
|         return followerCount; | ||||
|     } | ||||
| 
 | ||||
|     public long getFollowingCount() { | ||||
|         return followingCount; | ||||
|     } | ||||
| 
 | ||||
|     public long getFollowingTagCount() { | ||||
|         return followingTagCount; | ||||
|     } | ||||
| 
 | ||||
|     public String getBiography() { | ||||
|         return biography; | ||||
|     } | ||||
| 
 | ||||
|     public String getExternalUrl() { | ||||
|         return externalUrl; | ||||
|     } | ||||
| 
 | ||||
|     public long getUsertagsCount() { | ||||
|         return usertagsCount; | ||||
|     } | ||||
| 
 | ||||
|     public String getPublicEmail() { | ||||
|         return publicEmail; | ||||
|     } | ||||
| 
 | ||||
|     public String getProfileContext() { | ||||
|         return profileContext; | ||||
|     } | ||||
| 
 | ||||
|     public String getSocialContext() { | ||||
|         return socialContext; | ||||
|     } | ||||
| 
 | ||||
|     public List<UserProfileContextLink> getProfileContextLinks() { | ||||
|         return profileContextLinksWithUserIds; | ||||
|     } | ||||
| 
 | ||||
|     public String getFbId() { | ||||
|         return interopMessagingUserFbid; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean equals(final Object o) { | ||||
|         if (this == o) return true; | ||||
|         if (o == null || getClass() != o.getClass()) return false; | ||||
|         final User user = (User) o; | ||||
|         return pk == user.pk && | ||||
|                 isPrivate == user.isPrivate && | ||||
|                 isVerified == user.isVerified && | ||||
|                 hasAnonymousProfilePicture == user.hasAnonymousProfilePicture && | ||||
|                 isUnpublished == user.isUnpublished && | ||||
|                 isFavorite == user.isFavorite && | ||||
|                 isDirectappInstalled == user.isDirectappInstalled && | ||||
|                 mediaCount == user.mediaCount && | ||||
|                 followerCount == user.followerCount && | ||||
|                 followingCount == user.followingCount && | ||||
|                 followingTagCount == user.followingTagCount && | ||||
|                 usertagsCount == user.usertagsCount && | ||||
|                 Objects.equals(username, user.username) && | ||||
|                 Objects.equals(fullName, user.fullName) && | ||||
|                 Objects.equals(profilePicUrl, user.profilePicUrl) && | ||||
|                 Objects.equals(profilePicId, user.profilePicId) && | ||||
|                 Objects.equals(friendshipStatus, user.friendshipStatus) && | ||||
|                 Objects.equals(reelAutoArchive, user.reelAutoArchive) && | ||||
|                 Objects.equals(allowedCommenterType, user.allowedCommenterType) && | ||||
|                 Objects.equals(biography, user.biography) && | ||||
|                 Objects.equals(externalUrl, user.externalUrl) && | ||||
|                 Objects.equals(publicEmail, user.publicEmail); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int hashCode() { | ||||
|         return Objects.hash(pk, username, fullName, isPrivate, profilePicUrl, profilePicId, friendshipStatus, isVerified, hasAnonymousProfilePicture, | ||||
|                             isUnpublished, isFavorite, isDirectappInstalled, hasChaining, reelAutoArchive, allowedCommenterType, mediaCount, | ||||
|                             followerCount, followingCount, followingTagCount, biography, externalUrl, usertagsCount, publicEmail); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,38 @@ | ||||
| package awais.instagrabber.repositories.responses | ||||
| 
 | ||||
| import java.io.Serializable | ||||
| 
 | ||||
| 
 | ||||
| data class User @JvmOverloads constructor( | ||||
|     val pk: Long = 0, | ||||
|     val username: String = "", | ||||
|     val fullName: String = "", | ||||
|     val isPrivate: Boolean = false, | ||||
|     val profilePicUrl: String? = null, | ||||
|     val isVerified: Boolean = false, | ||||
|     val profilePicId: String? = null, | ||||
|     var friendshipStatus: FriendshipStatus? = null, | ||||
|     val hasAnonymousProfilePicture: Boolean = false, | ||||
|     val isUnpublished: Boolean = false, | ||||
|     val isFavorite: Boolean = false, | ||||
|     val isDirectappInstalled: Boolean = false, | ||||
|     val hasChaining: Boolean = false, | ||||
|     val reelAutoArchive: String? = null, | ||||
|     val allowedCommenterType: String? = null, | ||||
|     val mediaCount: Long = 0, | ||||
|     val followerCount: Long = 0, | ||||
|     val followingCount: Long = 0, | ||||
|     val followingTagCount: Long = 0, | ||||
|     val biography: String? = null, | ||||
|     val externalUrl: String? = null, | ||||
|     val usertagsCount: Long = 0, | ||||
|     val publicEmail: String? = null, | ||||
|     val hdProfilePicUrlInfo: HdProfilePicUrlInfo? = null, | ||||
|     val profileContext: String? = null, // "also followed by" your friends | ||||
|     val profileContextLinksWithUserIds: List<UserProfileContextLink>? = null, // ^ | ||||
|     val socialContext: String? = null, // AYML | ||||
|     val interopMessagingUserFbid: String? = null, // in DMs only: Facebook user ID | ||||
| ) : Serializable { | ||||
|     val hDProfilePicUrl: String | ||||
|         get() = hdProfilePicUrlInfo?.url ?: profilePicUrl ?: "" | ||||
| } | ||||
| @ -7,7 +7,6 @@ import android.util.Log | ||||
| import android.webkit.CookieManager | ||||
| import awais.instagrabber.db.datasources.AccountDataSource | ||||
| import awais.instagrabber.db.repositories.AccountRepository | ||||
| import awais.instagrabber.db.repositories.RepositoryCallback | ||||
| import java.net.CookiePolicy | ||||
| import java.net.HttpCookie | ||||
| import java.net.URI | ||||
| @ -48,14 +47,9 @@ fun setupCookies(cookieRaw: String) { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fun removeAllAccounts(context: Context, callback: RepositoryCallback<Void?>?) { | ||||
| suspend fun removeAllAccounts(context: Context) { | ||||
|     NET_COOKIE_MANAGER.cookieStore.removeAll() | ||||
|     try { | ||||
|         AccountRepository.getInstance(AccountDataSource.getInstance(context)) | ||||
|             .deleteAllAccounts(callback) | ||||
|     } catch (e: Exception) { | ||||
|         Log.e(TAG, "setupCookies", e) | ||||
|     } | ||||
|     AccountRepository.getInstance(AccountDataSource.getInstance(context)).deleteAllAccounts() | ||||
| } | ||||
| 
 | ||||
| fun getUserIdFromCookie(cookies: String?): Long { | ||||
|  | ||||
| @ -42,6 +42,7 @@ import awais.instagrabber.db.repositories.RepositoryCallback; | ||||
| import awais.instagrabber.interfaces.FetchListener; | ||||
| import awais.instagrabber.models.enums.FavoriteType; | ||||
| import awais.instagrabber.utils.PasswordUtils.IncorrectPasswordException; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| 
 | ||||
| import static awais.instagrabber.utils.Utils.settingsHelper; | ||||
| 
 | ||||
| @ -396,33 +397,32 @@ public final class ExportImportUtils { | ||||
|     private static ListenableFuture<JSONArray> getCookies(final Context context) { | ||||
|         final SettableFuture<JSONArray> future = SettableFuture.create(); | ||||
|         final AccountRepository accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(context)); | ||||
|         accountRepository.getAllAccounts(new RepositoryCallback<List<Account>>() { | ||||
|             @Override | ||||
|             public void onSuccess(final List<Account> accounts) { | ||||
|                 final JSONArray jsonArray = new JSONArray(); | ||||
|                 try { | ||||
|                     for (final Account cookie : accounts) { | ||||
|                         final JSONObject jsonObject = new JSONObject(); | ||||
|                         jsonObject.put("i", cookie.getUid()); | ||||
|                         jsonObject.put("u", cookie.getUsername()); | ||||
|                         jsonObject.put("c", cookie.getCookie()); | ||||
|                         jsonObject.put("full_name", cookie.getFullName()); | ||||
|                         jsonObject.put("profile_pic", cookie.getProfilePic()); | ||||
|                         jsonArray.put(jsonObject); | ||||
|         accountRepository.getAllAccounts( | ||||
|                 CoroutineUtilsKt.getContinuation((accounts, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { | ||||
|                     if (throwable != null) { | ||||
|                         Log.e(TAG, "getCookies: ", throwable); | ||||
|                         future.set(new JSONArray()); | ||||
|                         return; | ||||
|                     } | ||||
|                 } catch (Exception e) { | ||||
|                     if (BuildConfig.DEBUG) { | ||||
|                         Log.e(TAG, "Error exporting accounts", e); | ||||
|                     final JSONArray jsonArray = new JSONArray(); | ||||
|                     try { | ||||
|                         for (final Account cookie : accounts) { | ||||
|                             final JSONObject jsonObject = new JSONObject(); | ||||
|                             jsonObject.put("i", cookie.getUid()); | ||||
|                             jsonObject.put("u", cookie.getUsername()); | ||||
|                             jsonObject.put("c", cookie.getCookie()); | ||||
|                             jsonObject.put("full_name", cookie.getFullName()); | ||||
|                             jsonObject.put("profile_pic", cookie.getProfilePic()); | ||||
|                             jsonArray.put(jsonObject); | ||||
|                         } | ||||
|                     } catch (Exception e) { | ||||
|                         if (BuildConfig.DEBUG) { | ||||
|                             Log.e(TAG, "Error exporting accounts", e); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 future.set(jsonArray); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onDataNotAvailable() { | ||||
|                 future.set(new JSONArray()); | ||||
|             } | ||||
|         }); | ||||
|                     future.set(jsonArray); | ||||
|                 }), Dispatchers.getIO()) | ||||
|         ); | ||||
|         return future; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -12,9 +12,10 @@ import androidx.lifecycle.MutableLiveData; | ||||
| import awais.instagrabber.repositories.responses.User; | ||||
| import awais.instagrabber.utils.Constants; | ||||
| import awais.instagrabber.utils.CookieUtils; | ||||
| import awais.instagrabber.utils.CoroutineUtilsKt; | ||||
| import awais.instagrabber.utils.TextUtils; | ||||
| import awais.instagrabber.webservices.ServiceCallback; | ||||
| import awais.instagrabber.webservices.UserService; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| 
 | ||||
| import static awais.instagrabber.utils.Utils.settingsHelper; | ||||
| 
 | ||||
| @ -32,7 +33,7 @@ public class AppStateViewModel extends AndroidViewModel { | ||||
|         cookie = settingsHelper.getString(Constants.COOKIE); | ||||
|         final boolean isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != 0; | ||||
|         if (!isLoggedIn) return; | ||||
|         userService = UserService.getInstance(); | ||||
|         userService = UserService.INSTANCE; | ||||
|         // final AccountRepository accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(application)); | ||||
|         fetchProfileDetails(); | ||||
|     } | ||||
| @ -49,16 +50,12 @@ public class AppStateViewModel extends AndroidViewModel { | ||||
|     private void fetchProfileDetails() { | ||||
|         final long uid = CookieUtils.getUserIdFromCookie(cookie); | ||||
|         if (userService == null) return; | ||||
|         userService.getUserInfo(uid, new ServiceCallback<User>() { | ||||
|             @Override | ||||
|             public void onSuccess(final User user) { | ||||
|                 currentUser.postValue(user); | ||||
|         userService.getUserInfo(uid, CoroutineUtilsKt.getContinuation((user, throwable) -> { | ||||
|             if (throwable != null) { | ||||
|                 Log.e(TAG, "onFailure: ", throwable); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(final Throwable t) { | ||||
|                 Log.e(TAG, "onFailure: ", t); | ||||
|             } | ||||
|         }); | ||||
|             currentUser.postValue(user); | ||||
|         }, Dispatchers.getIO())); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -30,13 +30,13 @@ import awais.instagrabber.repositories.responses.CommentsFetchResponse; | ||||
| import awais.instagrabber.repositories.responses.User; | ||||
| import awais.instagrabber.utils.Constants; | ||||
| import awais.instagrabber.utils.CookieUtils; | ||||
| import awais.instagrabber.utils.CoroutineUtilsKt; | ||||
| import awais.instagrabber.utils.Utils; | ||||
| import awais.instagrabber.webservices.CommentService; | ||||
| import awais.instagrabber.webservices.GraphQLService; | ||||
| import awais.instagrabber.webservices.ServiceCallback; | ||||
| import retrofit2.Call; | ||||
| import retrofit2.Callback; | ||||
| import retrofit2.Response; | ||||
| import kotlin.coroutines.Continuation; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| 
 | ||||
| import static awais.instagrabber.utils.Utils.settingsHelper; | ||||
| 
 | ||||
| @ -113,7 +113,7 @@ public class CommentsViewerViewModel extends ViewModel { | ||||
|     }; | ||||
| 
 | ||||
|     public CommentsViewerViewModel() { | ||||
|         graphQLService = GraphQLService.getInstance(); | ||||
|         graphQLService = GraphQLService.INSTANCE; | ||||
|         final String cookie = settingsHelper.getString(Constants.COOKIE); | ||||
|         final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); | ||||
|         final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); | ||||
| @ -165,8 +165,12 @@ public class CommentsViewerViewModel extends ViewModel { | ||||
|             commentService.fetchComments(postId, rootCursor, ccb); | ||||
|             return; | ||||
|         } | ||||
|         final Call<String> request = graphQLService.fetchComments(shortCode, true, rootCursor); | ||||
|         enqueueRequest(request, true, shortCode, ccb); | ||||
|         graphQLService.fetchComments( | ||||
|                 shortCode, | ||||
|                 true, | ||||
|                 rootCursor, | ||||
|                 enqueueRequest(true, shortCode, ccb) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     public void fetchReplies() { | ||||
| @ -190,54 +194,49 @@ public class CommentsViewerViewModel extends ViewModel { | ||||
|             commentService.fetchChildComments(postId, commentId, repliesCursor, rcb); | ||||
|             return; | ||||
|         } | ||||
|         final Call<String> request = graphQLService.fetchComments(commentId, false, repliesCursor); | ||||
|         enqueueRequest(request, false, commentId, rcb); | ||||
|         graphQLService.fetchComments(commentId, false, repliesCursor, enqueueRequest(false, commentId, rcb)); | ||||
|     } | ||||
| 
 | ||||
|     private void enqueueRequest(@NonNull final Call<String> request, | ||||
|                                 final boolean root, | ||||
|                                 final String shortCodeOrCommentId, | ||||
|                                 final ServiceCallback callback) { | ||||
|         request.enqueue(new Callback<String>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||
|                 final String rawBody = response.body(); | ||||
|                 if (rawBody == null) { | ||||
|                     Log.e(TAG, "Error occurred while fetching gql comments of " + shortCodeOrCommentId); | ||||
|                     callback.onSuccess(null); | ||||
|                     return; | ||||
|                 } | ||||
|                 try { | ||||
|                     final JSONObject body = root ? new JSONObject(rawBody).getJSONObject("data") | ||||
|                                                                           .getJSONObject("shortcode_media") | ||||
|                                                                           .getJSONObject("edge_media_to_parent_comment") | ||||
|                                                  : new JSONObject(rawBody).getJSONObject("data") | ||||
|                                                                           .getJSONObject("comment") | ||||
|                                                                           .getJSONObject("edge_threaded_comments"); | ||||
|                     final int count = body.optInt("count"); | ||||
|                     final JSONObject pageInfo = body.getJSONObject("page_info"); | ||||
|                     final boolean hasNextPage = pageInfo.getBoolean("has_next_page"); | ||||
|                     final String endCursor = pageInfo.isNull("end_cursor") || !hasNextPage ? null : pageInfo.optString("end_cursor"); | ||||
|                     final JSONArray commentsJsonArray = body.getJSONArray("edges"); | ||||
|                     final ImmutableList.Builder<Comment> builder = ImmutableList.builder(); | ||||
|                     for (int i = 0; i < commentsJsonArray.length(); i++) { | ||||
|                         final Comment commentModel = getComment(commentsJsonArray.getJSONObject(i).getJSONObject("node"), root); | ||||
|                         builder.add(commentModel); | ||||
|                     } | ||||
|                     callback.onSuccess(root ? | ||||
|                                        new CommentsFetchResponse(count, endCursor, builder.build()) : | ||||
|                                        new ChildCommentsFetchResponse(count, endCursor, builder.build())); | ||||
|                 } catch (Exception e) { | ||||
|                     Log.e(TAG, "onResponse", e); | ||||
|                     callback.onFailure(e); | ||||
|                 } | ||||
|     private Continuation<String> enqueueRequest(final boolean root, | ||||
|                                                 final String shortCodeOrCommentId, | ||||
|                                                 @SuppressWarnings("rawtypes") final ServiceCallback callback) { | ||||
|         return CoroutineUtilsKt.getContinuation((response, throwable) -> { | ||||
|             if (throwable != null) { | ||||
|                 callback.onFailure(throwable); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||
|                 callback.onFailure(t); | ||||
|             if (response == null) { | ||||
|                 Log.e(TAG, "Error occurred while fetching gql comments of " + shortCodeOrCommentId); | ||||
|                 //noinspection unchecked | ||||
|                 callback.onSuccess(null); | ||||
|                 return; | ||||
|             } | ||||
|         }); | ||||
|             try { | ||||
|                 final JSONObject body = root ? new JSONObject(response).getJSONObject("data") | ||||
|                                                                        .getJSONObject("shortcode_media") | ||||
|                                                                        .getJSONObject("edge_media_to_parent_comment") | ||||
|                                              : new JSONObject(response).getJSONObject("data") | ||||
|                                                                        .getJSONObject("comment") | ||||
|                                                                        .getJSONObject("edge_threaded_comments"); | ||||
|                 final int count = body.optInt("count"); | ||||
|                 final JSONObject pageInfo = body.getJSONObject("page_info"); | ||||
|                 final boolean hasNextPage = pageInfo.getBoolean("has_next_page"); | ||||
|                 final String endCursor = pageInfo.isNull("end_cursor") || !hasNextPage ? null : pageInfo.optString("end_cursor"); | ||||
|                 final JSONArray commentsJsonArray = body.getJSONArray("edges"); | ||||
|                 final ImmutableList.Builder<Comment> builder = ImmutableList.builder(); | ||||
|                 for (int i = 0; i < commentsJsonArray.length(); i++) { | ||||
|                     final Comment commentModel = getComment(commentsJsonArray.getJSONObject(i).getJSONObject("node"), root); | ||||
|                     builder.add(commentModel); | ||||
|                 } | ||||
|                 final Object result = root ? new CommentsFetchResponse(count, endCursor, builder.build()) | ||||
|                                            : new ChildCommentsFetchResponse(count, endCursor, builder.build()); | ||||
|                 //noinspection unchecked | ||||
|                 callback.onSuccess(result); | ||||
|             } catch (Exception e) { | ||||
|                 Log.e(TAG, "onResponse", e); | ||||
|                 callback.onFailure(e); | ||||
|             } | ||||
|         }, Dispatchers.getIO()); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|  | ||||
| @ -135,7 +135,7 @@ class DirectSettingsViewModel( | ||||
|                 if (isAdmin) ACTION_REMOVE_ADMIN else ACTION_MAKE_ADMIN | ||||
|             )) | ||||
|         } | ||||
|         val blocking: Boolean = user.friendshipStatus.blocking | ||||
|         val blocking: Boolean = user.friendshipStatus?.blocking ?: false | ||||
|         options.add(Option( | ||||
|             if (blocking) getString(R.string.unblock) else getString(R.string.block), | ||||
|             if (blocking) ACTION_UNBLOCK else ACTION_BLOCK | ||||
| @ -144,7 +144,7 @@ class DirectSettingsViewModel( | ||||
|         // options.add(new Option<>(getString(R.string.report), ACTION_REPORT)); | ||||
|         val isGroup: Boolean? = threadManager.isGroup.value | ||||
|         if (isGroup != null && isGroup) { | ||||
|             val restricted: Boolean = user.friendshipStatus.isRestricted | ||||
|             val restricted: Boolean = user.friendshipStatus?.isRestricted ?: false | ||||
|             options.add(Option( | ||||
|                 if (restricted) getString(R.string.unrestrict) else getString(R.string.restrict), | ||||
|                 if (restricted) ACTION_UNRESTRICT else ACTION_RESTRICT | ||||
|  | ||||
| @ -40,12 +40,15 @@ class PostViewV2ViewModel : ViewModel() { | ||||
|     private val liked = MutableLiveData(false) | ||||
|     private val saved = MutableLiveData(false) | ||||
|     private val options = MutableLiveData<List<Int>>(ArrayList()) | ||||
|     private val viewerId: Long | ||||
|     val isLoggedIn: Boolean | ||||
|     private var messageManager: DirectMessagesManager? = null | ||||
|     private val cookie = Utils.settingsHelper.getString(Constants.COOKIE) | ||||
|     private val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID) | ||||
|     private val csrfToken = getCsrfTokenFromCookie(cookie) | ||||
|     private val viewerId = getUserIdFromCookie(cookie) | ||||
| 
 | ||||
|     lateinit var media: Media | ||||
|         private set | ||||
|     private var mediaService: MediaService? = null | ||||
|     private var messageManager: DirectMessagesManager? = null | ||||
|     val isLoggedIn = cookie.isNotBlank() && !csrfToken.isNullOrBlank() && viewerId != 0L | ||||
| 
 | ||||
|     fun setMedia(media: Media) { | ||||
|         this.media = media | ||||
| @ -125,11 +128,15 @@ class PostViewV2ViewModel : ViewModel() { | ||||
|     fun like(): LiveData<Resource<Any?>> { | ||||
|         val data = MutableLiveData<Resource<Any?>>() | ||||
|         data.postValue(loading(null)) | ||||
|         if (!isLoggedIn) { | ||||
|             data.postValue(error("Not logged in!", null)) | ||||
|             return data | ||||
|         } | ||||
|         viewModelScope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val mediaId = media.pk ?: return@launch | ||||
|                 val liked = mediaService?.like(mediaId) | ||||
|                 updateMediaLikeUnlike(data, liked ?: false) | ||||
|                 val liked = MediaService.like(csrfToken!!, viewerId, deviceUuid, mediaId) | ||||
|                 updateMediaLikeUnlike(data, liked) | ||||
|             } catch (e: Exception) { | ||||
|                 data.postValue(error(e.message, null)) | ||||
|             } | ||||
| @ -140,11 +147,15 @@ class PostViewV2ViewModel : ViewModel() { | ||||
|     fun unlike(): LiveData<Resource<Any?>> { | ||||
|         val data = MutableLiveData<Resource<Any?>>() | ||||
|         data.postValue(loading(null)) | ||||
|         if (!isLoggedIn) { | ||||
|             data.postValue(error("Not logged in!", null)) | ||||
|             return data | ||||
|         } | ||||
|         viewModelScope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val mediaId = media.pk ?: return@launch | ||||
|                 val unliked = mediaService?.unlike(mediaId) | ||||
|                 updateMediaLikeUnlike(data, unliked ?: false) | ||||
|                 val unliked = MediaService.unlike(csrfToken!!, viewerId, deviceUuid, mediaId) | ||||
|                 updateMediaLikeUnlike(data, unliked) | ||||
|             } catch (e: Exception) { | ||||
|                 data.postValue(error(e.message, null)) | ||||
|             } | ||||
| @ -185,11 +196,15 @@ class PostViewV2ViewModel : ViewModel() { | ||||
|     fun save(collection: String?, ignoreSaveState: Boolean): LiveData<Resource<Any?>> { | ||||
|         val data = MutableLiveData<Resource<Any?>>() | ||||
|         data.postValue(loading(null)) | ||||
|         if (!isLoggedIn) { | ||||
|             data.postValue(error("Not logged in!", null)) | ||||
|             return data | ||||
|         } | ||||
|         viewModelScope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val mediaId = media.pk ?: return@launch | ||||
|                 val saved = mediaService?.save(mediaId, collection) | ||||
|                 getSaveUnsaveCallback(data, saved ?: false, ignoreSaveState) | ||||
|                 val saved = MediaService.save(csrfToken!!, viewerId, deviceUuid, mediaId, collection) | ||||
|                 getSaveUnsaveCallback(data, saved, ignoreSaveState) | ||||
|             } catch (e: Exception) { | ||||
|                 data.postValue(error(e.message, null)) | ||||
|             } | ||||
| @ -200,10 +215,14 @@ class PostViewV2ViewModel : ViewModel() { | ||||
|     fun unsave(): LiveData<Resource<Any?>> { | ||||
|         val data = MutableLiveData<Resource<Any?>>() | ||||
|         data.postValue(loading(null)) | ||||
|         if (!isLoggedIn) { | ||||
|             data.postValue(error("Not logged in!", null)) | ||||
|             return data | ||||
|         } | ||||
|         viewModelScope.launch(Dispatchers.IO) { | ||||
|             val mediaId = media.pk ?: return@launch | ||||
|             val unsaved = mediaService?.unsave(mediaId) | ||||
|             getSaveUnsaveCallback(data, unsaved ?: false, false) | ||||
|             val unsaved = MediaService.unsave(csrfToken!!, viewerId, deviceUuid, mediaId) | ||||
|             getSaveUnsaveCallback(data, unsaved, false) | ||||
|         } | ||||
|         return data | ||||
|     } | ||||
| @ -225,11 +244,15 @@ class PostViewV2ViewModel : ViewModel() { | ||||
|     fun updateCaption(caption: String): LiveData<Resource<Any?>> { | ||||
|         val data = MutableLiveData<Resource<Any?>>() | ||||
|         data.postValue(loading(null)) | ||||
|         if (!isLoggedIn) { | ||||
|             data.postValue(error("Not logged in!", null)) | ||||
|             return data | ||||
|         } | ||||
|         viewModelScope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val postId = media.pk ?: return@launch | ||||
|                 val result = mediaService?.editCaption(postId, caption) | ||||
|                 if (result != null && result) { | ||||
|                 val result = MediaService.editCaption(csrfToken!!, viewerId, deviceUuid, postId, caption) | ||||
|                 if (result) { | ||||
|                     data.postValue(success("")) | ||||
|                     media.setPostCaption(caption) | ||||
|                     this@PostViewV2ViewModel.caption.postValue(media.caption) | ||||
| @ -255,8 +278,8 @@ class PostViewV2ViewModel : ViewModel() { | ||||
|         } | ||||
|         viewModelScope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val result = mediaService?.translate(pk, "1") | ||||
|                 if (result.isNullOrBlank()) { | ||||
|                 val result = MediaService.translate(pk, "1") | ||||
|                 if (result.isBlank()) { | ||||
|                     data.postValue(error("", null)) | ||||
|                     return@launch | ||||
|                 } | ||||
| @ -280,6 +303,10 @@ class PostViewV2ViewModel : ViewModel() { | ||||
|     fun delete(): LiveData<Resource<Any?>> { | ||||
|         val data = MutableLiveData<Resource<Any?>>() | ||||
|         data.postValue(loading(null)) | ||||
|         if (!isLoggedIn) { | ||||
|             data.postValue(error("Not logged in!", null)) | ||||
|             return data | ||||
|         } | ||||
|         val mediaId = media.id | ||||
|         val mediaType = media.mediaType | ||||
|         if (mediaId == null || mediaType == null) { | ||||
| @ -288,7 +315,7 @@ class PostViewV2ViewModel : ViewModel() { | ||||
|         } | ||||
|         viewModelScope.launch(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val response = mediaService?.delete(mediaId, mediaType) | ||||
|                 val response = MediaService.delete(csrfToken!!, viewerId, deviceUuid, mediaId, mediaType) | ||||
|                 if (response == null) { | ||||
|                     data.postValue(success(Any())) | ||||
|                     return@launch | ||||
| @ -317,15 +344,4 @@ class PostViewV2ViewModel : ViewModel() { | ||||
|         val mediaId = media.id ?: return | ||||
|         messageManager?.sendMedia(recipients, mediaId, viewModelScope) | ||||
|     } | ||||
| 
 | ||||
|     init { | ||||
|         val cookie = Utils.settingsHelper.getString(Constants.COOKIE) | ||||
|         val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID) | ||||
|         val csrfToken: String? = getCsrfTokenFromCookie(cookie) | ||||
|         viewerId = getUserIdFromCookie(cookie) | ||||
|         isLoggedIn = cookie.isNotBlank() && viewerId != 0L | ||||
|         if (!csrfToken.isNullOrBlank()) { | ||||
|             mediaService = MediaService.getInstance(deviceUuid, csrfToken, viewerId) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,21 @@ | ||||
| package awais.instagrabber.viewmodels | ||||
| 
 | ||||
| import androidx.lifecycle.* | ||||
| import awais.instagrabber.repositories.responses.User | ||||
| 
 | ||||
| class ProfileFragmentViewModel( | ||||
|     state: SavedStateHandle, | ||||
| ) : ViewModel() { | ||||
|     private val _profile = MutableLiveData<User?>() | ||||
|     val profile: LiveData<User?> = _profile | ||||
|     val username: LiveData<String> = Transformations.map(profile) { return@map it?.username ?: "" } | ||||
| 
 | ||||
|     var currentUser: User? = null | ||||
|     var isLoggedIn = false | ||||
|         get() = currentUser != null | ||||
|         private set | ||||
| 
 | ||||
|     init { | ||||
|         // Log.d(TAG, state.keys().toString()) | ||||
|     } | ||||
| } | ||||
| @ -24,7 +24,6 @@ import awais.instagrabber.R; | ||||
| import awais.instagrabber.fragments.UserSearchFragment; | ||||
| import awais.instagrabber.models.Resource; | ||||
| import awais.instagrabber.repositories.responses.User; | ||||
| import awais.instagrabber.repositories.responses.UserSearchResponse; | ||||
| import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; | ||||
| import awais.instagrabber.utils.Constants; | ||||
| import awais.instagrabber.utils.CookieUtils; | ||||
| @ -37,7 +36,6 @@ import awais.instagrabber.webservices.UserService; | ||||
| import kotlinx.coroutines.Dispatchers; | ||||
| import okhttp3.ResponseBody; | ||||
| import retrofit2.Call; | ||||
| import retrofit2.Callback; | ||||
| import retrofit2.Response; | ||||
| 
 | ||||
| import static awais.instagrabber.utils.Utils.settingsHelper; | ||||
| @ -72,8 +70,8 @@ public class UserSearchViewModel extends ViewModel { | ||||
|         if (TextUtils.isEmpty(csrfToken) || viewerId <= 0 || TextUtils.isEmpty(deviceUuid)) { | ||||
|             throw new IllegalArgumentException("User is not logged in!"); | ||||
|         } | ||||
|         userService = UserService.getInstance(); | ||||
|         directMessagesService = DirectMessagesService.getInstance(csrfToken, viewerId, deviceUuid); | ||||
|         userService = UserService.INSTANCE; | ||||
|         directMessagesService = DirectMessagesService.INSTANCE; | ||||
|         rankedRecipientsCache = RankedRecipientsCache.INSTANCE; | ||||
|         if ((rankedRecipientsCache.isFailed() || rankedRecipientsCache.isExpired()) && !rankedRecipientsCache.isUpdateInitiated()) { | ||||
|             updateRankedRecipientCache(); | ||||
| @ -170,9 +168,26 @@ public class UserSearchViewModel extends ViewModel { | ||||
|     } | ||||
| 
 | ||||
|     private void defaultUserSearch() { | ||||
|         searchRequest = userService.search(currentQuery); | ||||
|         //noinspection unchecked | ||||
|         handleRequest((Call<UserSearchResponse>) searchRequest); | ||||
|         userService.search(currentQuery, CoroutineUtilsKt.getContinuation((userSearchResponse, throwable) -> { | ||||
|             if (throwable != null) { | ||||
|                 Log.e(TAG, "onFailure: ", throwable); | ||||
|                 recipients.postValue(Resource.error(throwable.getMessage(), getCachedRecipients())); | ||||
|                 searchRequest = null; | ||||
|                 return; | ||||
|             } | ||||
|             if (userSearchResponse == null) { | ||||
|                 recipients.postValue(Resource.error(R.string.generic_null_response, getCachedRecipients())); | ||||
|                 searchRequest = null; | ||||
|                 return; | ||||
|             } | ||||
|             final List<RankedRecipient> list = userSearchResponse | ||||
|                     .getUsers() | ||||
|                     .stream() | ||||
|                     .map(RankedRecipient::of) | ||||
|                     .collect(Collectors.toList()); | ||||
|             recipients.postValue(Resource.success(mergeResponseWithCache(list))); | ||||
|             searchRequest = null; | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     private void rankedRecipientSearch() { | ||||
| @ -194,39 +209,6 @@ public class UserSearchViewModel extends ViewModel { | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private void handleRequest(@NonNull final Call<UserSearchResponse> request) { | ||||
|         request.enqueue(new Callback<UserSearchResponse>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<UserSearchResponse> call, @NonNull final Response<UserSearchResponse> response) { | ||||
|                 if (!response.isSuccessful()) { | ||||
|                     handleErrorResponse(response, true); | ||||
|                     searchRequest = null; | ||||
|                     return; | ||||
|                 } | ||||
|                 final UserSearchResponse userSearchResponse = response.body(); | ||||
|                 if (userSearchResponse == null) { | ||||
|                     recipients.postValue(Resource.error(R.string.generic_null_response, getCachedRecipients())); | ||||
|                     searchRequest = null; | ||||
|                     return; | ||||
|                 } | ||||
|                 final List<RankedRecipient> list = userSearchResponse | ||||
|                         .getUsers() | ||||
|                         .stream() | ||||
|                         .map(RankedRecipient::of) | ||||
|                         .collect(Collectors.toList()); | ||||
|                 recipients.postValue(Resource.success(mergeResponseWithCache(list))); | ||||
|                 searchRequest = null; | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<UserSearchResponse> call, @NonNull final Throwable t) { | ||||
|                 Log.e(TAG, "onFailure: ", t); | ||||
|                 recipients.postValue(Resource.error(t.getMessage(), getCachedRecipients())); | ||||
|                 searchRequest = null; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private List<RankedRecipient> mergeResponseWithCache(@NonNull final List<RankedRecipient> list) { | ||||
|         final Iterator<RankedRecipient> iterator = list.stream() | ||||
|                                                        .filter(Objects::nonNull) | ||||
|  | ||||
| @ -5,16 +5,11 @@ import awais.instagrabber.repositories.requests.directmessages.* | ||||
| import awais.instagrabber.repositories.responses.directmessages.* | ||||
| import awais.instagrabber.repositories.responses.giphy.GiphyGif | ||||
| import awais.instagrabber.utils.TextUtils.extractUrls | ||||
| import awais.instagrabber.utils.TextUtils.isEmpty | ||||
| import awais.instagrabber.utils.Utils | ||||
| import org.json.JSONArray | ||||
| import java.util.* | ||||
| 
 | ||||
| class DirectMessagesService private constructor( | ||||
|     val csrfToken: String, | ||||
|     val userId: Long, | ||||
|     val deviceUuid: String, | ||||
| ) : BaseService() { | ||||
| object DirectMessagesService : BaseService() { | ||||
|     private val repository: DirectMessagesRepository = RetrofitFactory.retrofit.create(DirectMessagesRepository::class.java) | ||||
| 
 | ||||
|     suspend fun fetchInbox( | ||||
| @ -55,6 +50,9 @@ class DirectMessagesService private constructor( | ||||
|     suspend fun fetchUnseenCount(): DirectBadgeCount = repository.fetchUnseenCount() | ||||
| 
 | ||||
|     suspend fun broadcastText( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         clientContext: String, | ||||
|         threadIdOrUserIds: ThreadIdOrUserIds, | ||||
|         text: String, | ||||
| @ -63,17 +61,20 @@ class DirectMessagesService private constructor( | ||||
|     ): DirectThreadBroadcastResponse { | ||||
|         val urls = extractUrls(text) | ||||
|         if (urls.isNotEmpty()) { | ||||
|             return broadcastLink(clientContext, threadIdOrUserIds, text, urls, repliedToItemId, repliedToClientContext) | ||||
|             return broadcastLink(csrfToken, userId, deviceUuid, clientContext, threadIdOrUserIds, text, urls, repliedToItemId, repliedToClientContext) | ||||
|         } | ||||
|         val broadcastOptions = TextBroadcastOptions(clientContext, threadIdOrUserIds, text) | ||||
|         if (!repliedToItemId.isNullOrBlank() && !repliedToClientContext.isNullOrBlank()) { | ||||
|             broadcastOptions.repliedToItemId = repliedToItemId | ||||
|             broadcastOptions.repliedToClientContext = repliedToClientContext | ||||
|         } | ||||
|         return broadcast(broadcastOptions) | ||||
|         return broadcast(csrfToken, userId, deviceUuid, broadcastOptions) | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun broadcastLink( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         clientContext: String, | ||||
|         threadIdOrUserIds: ThreadIdOrUserIds, | ||||
|         linkText: String, | ||||
| @ -86,75 +87,100 @@ class DirectMessagesService private constructor( | ||||
|             broadcastOptions.repliedToItemId = repliedToItemId | ||||
|             broadcastOptions.repliedToClientContext = repliedToClientContext | ||||
|         } | ||||
|         return broadcast(broadcastOptions) | ||||
|         return broadcast(csrfToken, userId, deviceUuid, broadcastOptions) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun broadcastPhoto( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         clientContext: String, | ||||
|         threadIdOrUserIds: ThreadIdOrUserIds, | ||||
|         uploadId: String, | ||||
|     ): DirectThreadBroadcastResponse { | ||||
|         return broadcast(PhotoBroadcastOptions(clientContext, threadIdOrUserIds, true, uploadId)) | ||||
|     } | ||||
|     ): DirectThreadBroadcastResponse = | ||||
|         broadcast(csrfToken, userId, deviceUuid, PhotoBroadcastOptions(clientContext, threadIdOrUserIds, true, uploadId)) | ||||
| 
 | ||||
|     suspend fun broadcastVideo( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         clientContext: String, | ||||
|         threadIdOrUserIds: ThreadIdOrUserIds, | ||||
|         uploadId: String, | ||||
|         videoResult: String, | ||||
|         sampled: Boolean, | ||||
|     ): DirectThreadBroadcastResponse { | ||||
|         return broadcast(VideoBroadcastOptions(clientContext, threadIdOrUserIds, videoResult, uploadId, sampled)) | ||||
|     } | ||||
|     ): DirectThreadBroadcastResponse = | ||||
|         broadcast(csrfToken, userId, deviceUuid, VideoBroadcastOptions(clientContext, threadIdOrUserIds, videoResult, uploadId, sampled)) | ||||
| 
 | ||||
|     suspend fun broadcastVoice( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         clientContext: String, | ||||
|         threadIdOrUserIds: ThreadIdOrUserIds, | ||||
|         uploadId: String, | ||||
|         waveform: List<Float>, | ||||
|         samplingFreq: Int, | ||||
|     ): DirectThreadBroadcastResponse { | ||||
|         return broadcast(VoiceBroadcastOptions(clientContext, threadIdOrUserIds, uploadId, waveform, samplingFreq)) | ||||
|     } | ||||
|     ): DirectThreadBroadcastResponse = | ||||
|         broadcast(csrfToken, userId, deviceUuid, VoiceBroadcastOptions(clientContext, threadIdOrUserIds, uploadId, waveform, samplingFreq)) | ||||
| 
 | ||||
|     suspend fun broadcastStoryReply( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         threadIdOrUserIds: ThreadIdOrUserIds, | ||||
|         text: String, | ||||
|         mediaId: String, | ||||
|         reelId: String, | ||||
|     ): DirectThreadBroadcastResponse { | ||||
|         return broadcast(StoryReplyBroadcastOptions(UUID.randomUUID().toString(), threadIdOrUserIds, text, mediaId, reelId)) | ||||
|     } | ||||
|     ): DirectThreadBroadcastResponse = | ||||
|         broadcast(csrfToken, userId, deviceUuid, StoryReplyBroadcastOptions(UUID.randomUUID().toString(), threadIdOrUserIds, text, mediaId, reelId)) | ||||
| 
 | ||||
|     suspend fun broadcastReaction( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         clientContext: String, | ||||
|         threadIdOrUserIds: ThreadIdOrUserIds, | ||||
|         itemId: String, | ||||
|         emoji: String?, | ||||
|         delete: Boolean, | ||||
|     ): DirectThreadBroadcastResponse { | ||||
|         return broadcast(ReactionBroadcastOptions(clientContext, threadIdOrUserIds, itemId, emoji, delete)) | ||||
|     } | ||||
|     ): DirectThreadBroadcastResponse = | ||||
|         broadcast(csrfToken, userId, deviceUuid, ReactionBroadcastOptions(clientContext, threadIdOrUserIds, itemId, emoji, delete)) | ||||
| 
 | ||||
|     suspend fun broadcastAnimatedMedia( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         clientContext: String, | ||||
|         threadIdOrUserIds: ThreadIdOrUserIds, | ||||
|         giphyGif: GiphyGif, | ||||
|     ): DirectThreadBroadcastResponse { | ||||
|         return broadcast(AnimatedMediaBroadcastOptions(clientContext, threadIdOrUserIds, giphyGif)) | ||||
|     } | ||||
|     ): DirectThreadBroadcastResponse = | ||||
|         broadcast(csrfToken, userId, deviceUuid, AnimatedMediaBroadcastOptions(clientContext, threadIdOrUserIds, giphyGif)) | ||||
| 
 | ||||
|     suspend fun broadcastMediaShare( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         clientContext: String, | ||||
|         threadIdOrUserIds: ThreadIdOrUserIds, | ||||
|         mediaId: String, | ||||
|     ): DirectThreadBroadcastResponse { | ||||
|         return broadcast(MediaShareBroadcastOptions(clientContext, threadIdOrUserIds, mediaId)) | ||||
|     } | ||||
|     ): DirectThreadBroadcastResponse = | ||||
|         broadcast(csrfToken, userId, deviceUuid, MediaShareBroadcastOptions(clientContext, threadIdOrUserIds, mediaId)) | ||||
| 
 | ||||
|     private suspend fun broadcast(broadcastOptions: BroadcastOptions): DirectThreadBroadcastResponse { | ||||
|         require(!isEmpty(broadcastOptions.clientContext)) { "Broadcast requires a valid client context value" } | ||||
|         val form = mutableMapOf<String, Any>() | ||||
|     private suspend fun broadcast( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         broadcastOptions: BroadcastOptions, | ||||
|     ): DirectThreadBroadcastResponse { | ||||
|         require(broadcastOptions.clientContext.isNotBlank()) { "Broadcast requires a valid client context value" } | ||||
|         val form = mutableMapOf<String, Any>( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uid" to userId, | ||||
|             "__uuid" to deviceUuid, | ||||
|             "client_context" to broadcastOptions.clientContext, | ||||
|             "mutation_token" to broadcastOptions.clientContext, | ||||
|         ) | ||||
|         val threadId = broadcastOptions.threadId | ||||
|         if (!threadId.isNullOrBlank()) { | ||||
|             form["thread_id"] = threadId | ||||
| @ -165,11 +191,6 @@ class DirectMessagesService private constructor( | ||||
|             } | ||||
|             form["recipient_users"] = JSONArray(userIds).toString() | ||||
|         } | ||||
|         form["_csrftoken"] = csrfToken | ||||
|         form["_uid"] = userId | ||||
|         form["__uuid"] = deviceUuid | ||||
|         form["client_context"] = broadcastOptions.clientContext | ||||
|         form["mutation_token"] = broadcastOptions.clientContext | ||||
|         val repliedToItemId = broadcastOptions.repliedToItemId | ||||
|         val repliedToClientContext = broadcastOptions.repliedToClientContext | ||||
|         if (!repliedToItemId.isNullOrBlank() && !repliedToClientContext.isNullOrBlank()) { | ||||
| @ -183,6 +204,8 @@ class DirectMessagesService private constructor( | ||||
|     } | ||||
| 
 | ||||
|     suspend fun addUsers( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|         userIds: Collection<Long>, | ||||
|     ): DirectThreadDetailsChangeResponse { | ||||
| @ -195,6 +218,8 @@ class DirectMessagesService private constructor( | ||||
|     } | ||||
| 
 | ||||
|     suspend fun removeUsers( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|         userIds: Collection<Long>, | ||||
|     ): String { | ||||
| @ -207,6 +232,8 @@ class DirectMessagesService private constructor( | ||||
|     } | ||||
| 
 | ||||
|     suspend fun updateTitle( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|         title: String, | ||||
|     ): DirectThreadDetailsChangeResponse { | ||||
| @ -219,6 +246,8 @@ class DirectMessagesService private constructor( | ||||
|     } | ||||
| 
 | ||||
|     suspend fun addAdmins( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|         userIds: Collection<Long>, | ||||
|     ): String { | ||||
| @ -231,6 +260,8 @@ class DirectMessagesService private constructor( | ||||
|     } | ||||
| 
 | ||||
|     suspend fun removeAdmins( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|         userIds: Collection<Long>, | ||||
|     ): String { | ||||
| @ -243,6 +274,8 @@ class DirectMessagesService private constructor( | ||||
|     } | ||||
| 
 | ||||
|     suspend fun deleteItem( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|         itemId: String, | ||||
|     ): String { | ||||
| @ -292,6 +325,9 @@ class DirectMessagesService private constructor( | ||||
|     } | ||||
| 
 | ||||
|     suspend fun createThread( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         userIds: List<Long>, | ||||
|         threadTitle: String?, | ||||
|     ): DirectThread { | ||||
| @ -309,7 +345,11 @@ class DirectMessagesService private constructor( | ||||
|         return repository.createThread(signedForm) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun mute(threadId: String): String { | ||||
|     suspend fun mute( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|     ): String { | ||||
|         val form = mapOf( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uuid" to deviceUuid | ||||
| @ -317,7 +357,11 @@ class DirectMessagesService private constructor( | ||||
|         return repository.mute(threadId, form) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun unmute(threadId: String): String { | ||||
|     suspend fun unmute( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|     ): String { | ||||
|         val form = mapOf( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uuid" to deviceUuid, | ||||
| @ -325,7 +369,11 @@ class DirectMessagesService private constructor( | ||||
|         return repository.unmute(threadId, form) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun muteMentions(threadId: String): String { | ||||
|     suspend fun muteMentions( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|     ): String { | ||||
|         val form = mapOf( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uuid" to deviceUuid, | ||||
| @ -333,7 +381,11 @@ class DirectMessagesService private constructor( | ||||
|         return repository.muteMentions(threadId, form) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun unmuteMentions(threadId: String): String { | ||||
|     suspend fun unmuteMentions( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|     ): String { | ||||
|         val form = mapOf( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uuid" to deviceUuid, | ||||
| @ -350,6 +402,8 @@ class DirectMessagesService private constructor( | ||||
|     } | ||||
| 
 | ||||
|     suspend fun approveParticipantRequests( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|         userIds: List<Long>, | ||||
|     ): DirectThreadDetailsChangeResponse { | ||||
| @ -363,6 +417,8 @@ class DirectMessagesService private constructor( | ||||
|     } | ||||
| 
 | ||||
|     suspend fun declineParticipantRequests( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|         userIds: List<Long>, | ||||
|     ): DirectThreadDetailsChangeResponse { | ||||
| @ -374,7 +430,11 @@ class DirectMessagesService private constructor( | ||||
|         return repository.declineParticipantRequests(threadId, form) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun approvalRequired(threadId: String): DirectThreadDetailsChangeResponse { | ||||
|     suspend fun approvalRequired( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|     ): DirectThreadDetailsChangeResponse { | ||||
|         val form = mapOf( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uuid" to deviceUuid, | ||||
| @ -382,7 +442,11 @@ class DirectMessagesService private constructor( | ||||
|         return repository.approvalRequired(threadId, form) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun approvalNotRequired(threadId: String): DirectThreadDetailsChangeResponse { | ||||
|     suspend fun approvalNotRequired( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|     ): DirectThreadDetailsChangeResponse { | ||||
|         val form = mapOf( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uuid" to deviceUuid, | ||||
| @ -390,7 +454,11 @@ class DirectMessagesService private constructor( | ||||
|         return repository.approvalNotRequired(threadId, form) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun leave(threadId: String): DirectThreadDetailsChangeResponse { | ||||
|     suspend fun leave( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|     ): DirectThreadDetailsChangeResponse { | ||||
|         val form = mapOf( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uuid" to deviceUuid, | ||||
| @ -398,7 +466,11 @@ class DirectMessagesService private constructor( | ||||
|         return repository.leave(threadId, form) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun end(threadId: String): DirectThreadDetailsChangeResponse { | ||||
|     suspend fun end( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|     ): DirectThreadDetailsChangeResponse { | ||||
|         val form = mapOf( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uuid" to deviceUuid, | ||||
| @ -423,7 +495,11 @@ class DirectMessagesService private constructor( | ||||
|         return repository.fetchPendingInbox(queryMap) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun approveRequest(threadId: String): String { | ||||
|     suspend fun approveRequest( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|     ): String { | ||||
|         val form = mapOf( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uuid" to deviceUuid, | ||||
| @ -431,7 +507,11 @@ class DirectMessagesService private constructor( | ||||
|         return repository.approveRequest(threadId, form) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun declineRequest(threadId: String): String { | ||||
|     suspend fun declineRequest( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|     ): String { | ||||
|         val form = mapOf( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uuid" to deviceUuid, | ||||
| @ -440,6 +520,8 @@ class DirectMessagesService private constructor( | ||||
|     } | ||||
| 
 | ||||
|     suspend fun markAsSeen( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         threadId: String, | ||||
|         directItem: DirectItem, | ||||
|     ): DirectItemSeenResponse? { | ||||
| @ -454,25 +536,4 @@ class DirectMessagesService private constructor( | ||||
|         ) | ||||
|         return repository.markItemSeen(threadId, itemId, form) | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private lateinit var instance: DirectMessagesService | ||||
| 
 | ||||
|         @JvmStatic | ||||
|         fun getInstance( | ||||
|             csrfToken: String, | ||||
|             userId: Long, | ||||
|             deviceUuid: String, | ||||
|         ): DirectMessagesService { | ||||
|             if (!this::instance.isInitialized | ||||
|                 || instance.csrfToken != csrfToken | ||||
|                 || instance.userId != userId | ||||
|                 || instance.deviceUuid != deviceUuid | ||||
|             ) { | ||||
|                 instance = DirectMessagesService(csrfToken, userId, deviceUuid) | ||||
|             } | ||||
|             return instance | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -1,264 +0,0 @@ | ||||
| package awais.instagrabber.webservices; | ||||
| 
 | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import org.json.JSONArray; | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Objects; | ||||
| 
 | ||||
| import awais.instagrabber.models.FollowModel; | ||||
| import awais.instagrabber.repositories.FriendshipRepository; | ||||
| import awais.instagrabber.repositories.responses.FriendshipChangeResponse; | ||||
| import awais.instagrabber.repositories.responses.FriendshipListFetchResponse; | ||||
| import awais.instagrabber.repositories.responses.FriendshipRestrictResponse; | ||||
| import awais.instagrabber.utils.TextUtils; | ||||
| import awais.instagrabber.utils.Utils; | ||||
| import retrofit2.Call; | ||||
| import retrofit2.Callback; | ||||
| import retrofit2.Response; | ||||
| 
 | ||||
| public class FriendshipService extends BaseService { | ||||
|     private static final String TAG = "FriendshipService"; | ||||
| 
 | ||||
|     private final FriendshipRepository repository; | ||||
|     private final String deviceUuid, csrfToken; | ||||
|     private final long userId; | ||||
| 
 | ||||
|     private static FriendshipService instance; | ||||
| 
 | ||||
|     private FriendshipService(final String deviceUuid, | ||||
|                               final String csrfToken, | ||||
|                               final long userId) { | ||||
|         this.deviceUuid = deviceUuid; | ||||
|         this.csrfToken = csrfToken; | ||||
|         this.userId = userId; | ||||
|         repository = RetrofitFactory.INSTANCE | ||||
|                                     .getRetrofit() | ||||
|                                     .create(FriendshipRepository.class); | ||||
|     } | ||||
| 
 | ||||
|     public String getCsrfToken() { | ||||
|         return csrfToken; | ||||
|     } | ||||
| 
 | ||||
|     public String getDeviceUuid() { | ||||
|         return deviceUuid; | ||||
|     } | ||||
| 
 | ||||
|     public long getUserId() { | ||||
|         return userId; | ||||
|     } | ||||
| 
 | ||||
|     public static FriendshipService getInstance(final String deviceUuid, final String csrfToken, final long userId) { | ||||
|         if (instance == null | ||||
|                 || !Objects.equals(instance.getCsrfToken(), csrfToken) | ||||
|                 || !Objects.equals(instance.getDeviceUuid(), deviceUuid) | ||||
|                 || !Objects.equals(instance.getUserId(), userId)) { | ||||
|             instance = new FriendshipService(deviceUuid, csrfToken, userId); | ||||
|         } | ||||
|         return instance; | ||||
|     } | ||||
| 
 | ||||
|     public void follow(final long targetUserId, | ||||
|                        final ServiceCallback<FriendshipChangeResponse> callback) { | ||||
|         change("create", targetUserId, callback); | ||||
|     } | ||||
| 
 | ||||
|     public void unfollow(final long targetUserId, | ||||
|                          final ServiceCallback<FriendshipChangeResponse> callback) { | ||||
|         change("destroy", targetUserId, callback); | ||||
|     } | ||||
| 
 | ||||
|     public void changeBlock(final boolean unblock, | ||||
|                             final long targetUserId, | ||||
|                             final ServiceCallback<FriendshipChangeResponse> callback) { | ||||
|         change(unblock ? "unblock" : "block", targetUserId, callback); | ||||
|     } | ||||
| 
 | ||||
|     public void toggleRestrict(final long targetUserId, | ||||
|                                final boolean restrict, | ||||
|                                final ServiceCallback<FriendshipRestrictResponse> callback) { | ||||
|         final Map<String, String> form = new HashMap<>(3); | ||||
|         form.put("_csrftoken", csrfToken); | ||||
|         form.put("_uuid", deviceUuid); | ||||
|         form.put("target_user_id", String.valueOf(targetUserId)); | ||||
|         final String action = restrict ? "restrict" : "unrestrict"; | ||||
|         final Call<FriendshipRestrictResponse> request = repository.toggleRestrict(action, form); | ||||
|         request.enqueue(new Callback<FriendshipRestrictResponse>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<FriendshipRestrictResponse> call, | ||||
|                                    @NonNull final Response<FriendshipRestrictResponse> response) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onSuccess(response.body()); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<FriendshipRestrictResponse> call, | ||||
|                                   @NonNull final Throwable t) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onFailure(t); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void approve(final long targetUserId, | ||||
|                         final ServiceCallback<FriendshipChangeResponse> callback) { | ||||
|         change("approve", targetUserId, callback); | ||||
|     } | ||||
| 
 | ||||
|     public void ignore(final long targetUserId, | ||||
|                        final ServiceCallback<FriendshipChangeResponse> callback) { | ||||
|         change("ignore", targetUserId, callback); | ||||
|     } | ||||
| 
 | ||||
|     public void removeFollower(final long targetUserId, | ||||
|                                final ServiceCallback<FriendshipChangeResponse> callback) { | ||||
|         change("remove_follower", targetUserId, callback); | ||||
|     } | ||||
| 
 | ||||
|     private void change(final String action, | ||||
|                         final long targetUserId, | ||||
|                         final ServiceCallback<FriendshipChangeResponse> callback) { | ||||
|         final Map<String, Object> form = new HashMap<>(5); | ||||
|         form.put("_csrftoken", csrfToken); | ||||
|         form.put("_uid", userId); | ||||
|         form.put("_uuid", deviceUuid); | ||||
|         form.put("radio_type", "wifi-none"); | ||||
|         form.put("user_id", targetUserId); | ||||
|         final Map<String, String> signedForm = Utils.sign(form); | ||||
|         final Call<FriendshipChangeResponse> request = repository.change(action, targetUserId, signedForm); | ||||
|         request.enqueue(new Callback<FriendshipChangeResponse>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<FriendshipChangeResponse> call, | ||||
|                                    @NonNull final Response<FriendshipChangeResponse> response) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onSuccess(response.body()); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<FriendshipChangeResponse> call, | ||||
|                                   @NonNull final Throwable t) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onFailure(t); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void changeMute(final boolean unmute, | ||||
|                            final long targetUserId, | ||||
|                            final boolean story, // true for story, false for posts | ||||
|                            final ServiceCallback<FriendshipChangeResponse> callback) { | ||||
|         final Map<String, String> form = new HashMap<>(4); | ||||
|         form.put("_csrftoken", csrfToken); | ||||
|         form.put("_uid", String.valueOf(userId)); | ||||
|         form.put("_uuid", deviceUuid); | ||||
|         form.put(story ? "target_reel_author_id" : "target_posts_author_id", String.valueOf(targetUserId)); | ||||
|         final Call<FriendshipChangeResponse> request = repository.changeMute(unmute ? | ||||
|                                                                              "unmute_posts_or_story_from_follow" : | ||||
|                                                                              "mute_posts_or_story_from_follow", | ||||
|                                                                              form); | ||||
|         request.enqueue(new Callback<FriendshipChangeResponse>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<FriendshipChangeResponse> call, | ||||
|                                    @NonNull final Response<FriendshipChangeResponse> response) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onSuccess(response.body()); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<FriendshipChangeResponse> call, | ||||
|                                   @NonNull final Throwable t) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onFailure(t); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void getList(final boolean follower, | ||||
|                         final long targetUserId, | ||||
|                         final String maxId, | ||||
|                         final ServiceCallback<FriendshipListFetchResponse> callback) { | ||||
|         final Map<String, String> queryMap = new HashMap<>(); | ||||
|         if (maxId != null) queryMap.put("max_id", maxId); | ||||
|         final Call<String> request = repository.getList( | ||||
|                 targetUserId, | ||||
|                 follower ? "followers" : "following", | ||||
|                 queryMap); | ||||
|         request.enqueue(new Callback<String>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||
|                 try { | ||||
|                     if (callback == null) { | ||||
|                         return; | ||||
|                     } | ||||
|                     final String body = response.body(); | ||||
|                     if (TextUtils.isEmpty(body)) { | ||||
|                         callback.onSuccess(null); | ||||
|                         return; | ||||
|                     } | ||||
|                     final FriendshipListFetchResponse friendshipListFetchResponse = parseListResponse(body); | ||||
|                     callback.onSuccess(friendshipListFetchResponse); | ||||
|                 } catch (JSONException e) { | ||||
|                     Log.e(TAG, "onResponse", e); | ||||
|                     callback.onFailure(e); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onFailure(t); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private FriendshipListFetchResponse parseListResponse(@NonNull final String body) throws JSONException { | ||||
|         final JSONObject root = new JSONObject(body); | ||||
|         final String nextMaxId = root.optString("next_max_id"); | ||||
|         final String status = root.optString("status"); | ||||
|         final JSONArray itemsJson = root.optJSONArray("users"); | ||||
|         final List<FollowModel> items = parseItems(itemsJson); | ||||
|         return new FriendshipListFetchResponse( | ||||
|                 nextMaxId, | ||||
|                 status, | ||||
|                 items | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private List<FollowModel> parseItems(final JSONArray items) throws JSONException { | ||||
|         if (items == null) { | ||||
|             return Collections.emptyList(); | ||||
|         } | ||||
|         final List<FollowModel> followModels = new ArrayList<>(); | ||||
|         for (int i = 0; i < items.length(); i++) { | ||||
|             final JSONObject itemJson = items.optJSONObject(i); | ||||
|             if (itemJson == null) { | ||||
|                 continue; | ||||
|             } | ||||
|             final FollowModel followModel = new FollowModel(itemJson.getString("pk"), | ||||
|                                                             itemJson.getString("username"), | ||||
|                                                             itemJson.optString("full_name"), | ||||
|                                                             itemJson.getString("profile_pic_url")); | ||||
|             if (followModel != null) { | ||||
|                 followModels.add(followModel); | ||||
|             } | ||||
|         } | ||||
|         return followModels; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,155 @@ | ||||
| package awais.instagrabber.webservices | ||||
| 
 | ||||
| import awais.instagrabber.models.FollowModel | ||||
| import awais.instagrabber.repositories.FriendshipRepository | ||||
| import awais.instagrabber.repositories.responses.FriendshipChangeResponse | ||||
| import awais.instagrabber.repositories.responses.FriendshipListFetchResponse | ||||
| import awais.instagrabber.repositories.responses.FriendshipRestrictResponse | ||||
| import awais.instagrabber.utils.Utils | ||||
| import awais.instagrabber.webservices.RetrofitFactory.retrofit | ||||
| import org.json.JSONArray | ||||
| import org.json.JSONException | ||||
| import org.json.JSONObject | ||||
| 
 | ||||
| object FriendshipService : BaseService() { | ||||
|     private val repository: FriendshipRepository = retrofit.create(FriendshipRepository::class.java) | ||||
| 
 | ||||
|     suspend fun follow( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         targetUserId: Long, | ||||
|     ): FriendshipChangeResponse = change(csrfToken, userId, deviceUuid, "create", targetUserId) | ||||
| 
 | ||||
|     suspend fun unfollow( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         targetUserId: Long, | ||||
|     ): FriendshipChangeResponse = change(csrfToken, userId, deviceUuid, "destroy", targetUserId) | ||||
| 
 | ||||
|     suspend fun changeBlock( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         unblock: Boolean, | ||||
|         targetUserId: Long, | ||||
|     ): FriendshipChangeResponse { | ||||
|         return change(csrfToken, userId, deviceUuid, if (unblock) "unblock" else "block", targetUserId) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun toggleRestrict( | ||||
|         csrfToken: String, | ||||
|         deviceUuid: String, | ||||
|         targetUserId: Long, | ||||
|         restrict: Boolean, | ||||
|     ): FriendshipRestrictResponse { | ||||
|         val form = mapOf( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uuid" to deviceUuid, | ||||
|             "target_user_id" to targetUserId.toString(), | ||||
|         ) | ||||
|         val action = if (restrict) "restrict" else "unrestrict" | ||||
|         return repository.toggleRestrict(action, form) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun approve( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         targetUserId: Long, | ||||
|     ): FriendshipChangeResponse = change(csrfToken, userId, deviceUuid, "approve", targetUserId) | ||||
| 
 | ||||
|     suspend fun ignore( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         targetUserId: Long, | ||||
|     ): FriendshipChangeResponse = change(csrfToken, userId, deviceUuid, "ignore", targetUserId) | ||||
| 
 | ||||
|     suspend fun removeFollower( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         targetUserId: Long, | ||||
|     ): FriendshipChangeResponse = change(csrfToken, userId, deviceUuid, "remove_follower", targetUserId) | ||||
| 
 | ||||
|     private suspend fun change( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         action: String, | ||||
|         targetUserId: Long, | ||||
|     ): FriendshipChangeResponse { | ||||
|         val form = mapOf( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uid" to userId, | ||||
|             "_uuid" to deviceUuid, | ||||
|             "radio_type" to "wifi-none", | ||||
|             "user_id" to targetUserId, | ||||
|         ) | ||||
|         val signedForm = Utils.sign(form) | ||||
|         return repository.change(action, targetUserId, signedForm) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun changeMute( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         unmute: Boolean, | ||||
|         targetUserId: Long, | ||||
|         story: Boolean,  // true for story, false for posts | ||||
|     ): FriendshipChangeResponse { | ||||
|         val form = mapOf( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uid" to userId.toString(), | ||||
|             "_uuid" to deviceUuid, | ||||
|             (if (story) "target_reel_author_id" else "target_posts_author_id") to targetUserId.toString(), | ||||
|         ) | ||||
|         return repository.changeMute( | ||||
|             if (unmute) "unmute_posts_or_story_from_follow" else "mute_posts_or_story_from_follow", | ||||
|             form | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun getList( | ||||
|         follower: Boolean, | ||||
|         targetUserId: Long, | ||||
|         maxId: String?, | ||||
|     ): FriendshipListFetchResponse { | ||||
|         val queryMap = if (maxId != null) mapOf("max_id" to maxId) else emptyMap() | ||||
|         val response = repository.getList(targetUserId, if (follower) "followers" else "following", queryMap) | ||||
|         return parseListResponse(response) | ||||
|     } | ||||
| 
 | ||||
|     @Throws(JSONException::class) | ||||
|     private fun parseListResponse(body: String): FriendshipListFetchResponse { | ||||
|         val root = JSONObject(body) | ||||
|         val nextMaxId = root.optString("next_max_id") | ||||
|         val status = root.optString("status") | ||||
|         val itemsJson = root.optJSONArray("users") | ||||
|         val items = parseItems(itemsJson) | ||||
|         return FriendshipListFetchResponse( | ||||
|             nextMaxId, | ||||
|             status, | ||||
|             items | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     @Throws(JSONException::class) | ||||
|     private fun parseItems(items: JSONArray?): List<FollowModel> { | ||||
|         if (items == null) { | ||||
|             return emptyList() | ||||
|         } | ||||
|         val followModels = mutableListOf<FollowModel>() | ||||
|         for (i in 0 until items.length()) { | ||||
|             val itemJson = items.optJSONObject(i) ?: continue | ||||
|             val followModel = FollowModel(itemJson.getString("pk"), | ||||
|                 itemJson.getString("username"), | ||||
|                 itemJson.optString("full_name"), | ||||
|                 itemJson.getString("profile_pic_url")) | ||||
|             followModels.add(followModel) | ||||
|         } | ||||
|         return followModels | ||||
|     } | ||||
| } | ||||
| @ -1,483 +0,0 @@ | ||||
| package awais.instagrabber.webservices; | ||||
| 
 | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import com.google.common.collect.ImmutableMap; | ||||
| 
 | ||||
| import org.json.JSONArray; | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| import awais.instagrabber.models.enums.FollowingType; | ||||
| import awais.instagrabber.repositories.GraphQLRepository; | ||||
| import awais.instagrabber.repositories.responses.FriendshipStatus; | ||||
| import awais.instagrabber.repositories.responses.GraphQLUserListFetchResponse; | ||||
| import awais.instagrabber.repositories.responses.Hashtag; | ||||
| import awais.instagrabber.repositories.responses.Location; | ||||
| import awais.instagrabber.repositories.responses.Media; | ||||
| import awais.instagrabber.repositories.responses.PostsFetchResponse; | ||||
| import awais.instagrabber.repositories.responses.User; | ||||
| import awais.instagrabber.utils.Constants; | ||||
| import awais.instagrabber.utils.ResponseBodyUtils; | ||||
| import awais.instagrabber.utils.TextUtils; | ||||
| import retrofit2.Call; | ||||
| import retrofit2.Callback; | ||||
| import retrofit2.Response; | ||||
| 
 | ||||
| public class GraphQLService extends BaseService { | ||||
|     private static final String TAG = "GraphQLService"; | ||||
| 
 | ||||
|     private final GraphQLRepository repository; | ||||
| 
 | ||||
|     private static GraphQLService instance; | ||||
| 
 | ||||
|     private GraphQLService() { | ||||
|         repository = RetrofitFactory.INSTANCE | ||||
|                                     .getRetrofitWeb() | ||||
|                                     .create(GraphQLRepository.class); | ||||
|     } | ||||
| 
 | ||||
|     public static GraphQLService getInstance() { | ||||
|         if (instance == null) { | ||||
|             instance = new GraphQLService(); | ||||
|         } | ||||
|         return instance; | ||||
|     } | ||||
| 
 | ||||
|     // TODO convert string response to a response class | ||||
|     private void fetch(final String queryHash, | ||||
|                        final String variables, | ||||
|                        final String arg1, | ||||
|                        final String arg2, | ||||
|                        final User backup, | ||||
|                        final ServiceCallback<PostsFetchResponse> callback) { | ||||
|         final Map<String, String> queryMap = new HashMap<>(); | ||||
|         queryMap.put("query_hash", queryHash); | ||||
|         queryMap.put("variables", variables); | ||||
|         final Call<String> request = repository.fetch(queryMap); | ||||
|         request.enqueue(new Callback<String>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||
|                 try { | ||||
|                     // Log.d(TAG, "onResponse: body: " + response.body()); | ||||
|                     final PostsFetchResponse postsFetchResponse = parsePostResponse(response, arg1, arg2, backup); | ||||
|                     if (callback != null) { | ||||
|                         callback.onSuccess(postsFetchResponse); | ||||
|                     } | ||||
|                 } catch (JSONException e) { | ||||
|                     Log.e(TAG, "onResponse", e); | ||||
|                     if (callback != null) { | ||||
|                         callback.onFailure(e); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onFailure(t); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void fetchLocationPosts(final long locationId, | ||||
|                                    final String maxId, | ||||
|                                    final ServiceCallback<PostsFetchResponse> callback) { | ||||
|         fetch("36bd0f2bf5911908de389b8ceaa3be6d", | ||||
|               "{\"id\":\"" + locationId + "\"," + | ||||
|                       "\"first\":25," + | ||||
|                       "\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", | ||||
|               Constants.EXTRAS_LOCATION, | ||||
|               "edge_location_to_media", | ||||
|               null, | ||||
|               callback); | ||||
|     } | ||||
| 
 | ||||
|     public void fetchHashtagPosts(@NonNull final String tag, | ||||
|                                   final String maxId, | ||||
|                                   final ServiceCallback<PostsFetchResponse> callback) { | ||||
|         fetch("9b498c08113f1e09617a1703c22b2f32", | ||||
|               "{\"tag_name\":\"" + tag + "\"," + | ||||
|                       "\"first\":25," + | ||||
|                       "\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", | ||||
|               Constants.EXTRAS_HASHTAG, | ||||
|               "edge_hashtag_to_media", | ||||
|               null, | ||||
|               callback); | ||||
|     } | ||||
| 
 | ||||
|     public void fetchProfilePosts(final long profileId, | ||||
|                                   final int postsPerPage, | ||||
|                                   final String maxId, | ||||
|                                   final User backup, | ||||
|                                   final ServiceCallback<PostsFetchResponse> callback) { | ||||
|         fetch("02e14f6a7812a876f7d133c9555b1151", | ||||
|               "{\"id\":\"" + profileId + "\"," + | ||||
|                       "\"first\":" + postsPerPage + "," + | ||||
|                       "\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", | ||||
|               Constants.EXTRAS_USER, | ||||
|               "edge_owner_to_timeline_media", | ||||
|               backup, | ||||
|               callback); | ||||
|     } | ||||
| 
 | ||||
|     public void fetchTaggedPosts(final long profileId, | ||||
|                                  final int postsPerPage, | ||||
|                                  final String maxId, | ||||
|                                  final ServiceCallback<PostsFetchResponse> callback) { | ||||
|         fetch("31fe64d9463cbbe58319dced405c6206", | ||||
|               "{\"id\":\"" + profileId + "\"," + | ||||
|                       "\"first\":" + postsPerPage + "," + | ||||
|                       "\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", | ||||
|               Constants.EXTRAS_USER, | ||||
|               "edge_user_to_photos_of_you", | ||||
|               null, | ||||
|               callback); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private PostsFetchResponse parsePostResponse(@NonNull final Response<String> response, | ||||
|                                                  @NonNull final String arg1, | ||||
|                                                  @NonNull final String arg2, | ||||
|                                                  final User backup) | ||||
|             throws JSONException { | ||||
|         if (TextUtils.isEmpty(response.body())) { | ||||
|             Log.e(TAG, "parseResponse: feed response body is empty with status code: " + response.code()); | ||||
|             return new PostsFetchResponse(Collections.emptyList(), false, null); | ||||
|         } | ||||
|         return parseResponseBody(response.body(), arg1, arg2, backup); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private PostsFetchResponse parseResponseBody(@NonNull final String body, | ||||
|                                                  @NonNull final String arg1, | ||||
|                                                  @NonNull final String arg2, | ||||
|                                                  final User backup) | ||||
|             throws JSONException { | ||||
|         final List<Media> items = new ArrayList<>(); | ||||
|         final JSONObject timelineFeed = new JSONObject(body) | ||||
|                 .getJSONObject("data") | ||||
|                 .getJSONObject(arg1) | ||||
|                 .getJSONObject(arg2); | ||||
|         final String endCursor; | ||||
|         final boolean hasNextPage; | ||||
| 
 | ||||
|         final JSONObject pageInfo = timelineFeed.getJSONObject("page_info"); | ||||
|         if (pageInfo.has("has_next_page")) { | ||||
|             hasNextPage = pageInfo.getBoolean("has_next_page"); | ||||
|             endCursor = hasNextPage ? pageInfo.getString("end_cursor") : null; | ||||
|         } else { | ||||
|             hasNextPage = false; | ||||
|             endCursor = null; | ||||
|         } | ||||
| 
 | ||||
|         final JSONArray feedItems = timelineFeed.getJSONArray("edges"); | ||||
| 
 | ||||
|         for (int i = 0; i < feedItems.length(); ++i) { | ||||
|             final JSONObject itemJson = feedItems.optJSONObject(i); | ||||
|             if (itemJson == null) { | ||||
|                 continue; | ||||
|             } | ||||
|             final Media media = ResponseBodyUtils.parseGraphQLItem(itemJson, backup); | ||||
|             if (media != null) { | ||||
|                 items.add(media); | ||||
|             } | ||||
|         } | ||||
|         return new PostsFetchResponse(items, hasNextPage, endCursor); | ||||
|     } | ||||
| 
 | ||||
|     // TODO convert string response to a response class | ||||
|     public void fetchCommentLikers(final String commentId, | ||||
|                                    final String endCursor, | ||||
|                                    final ServiceCallback<GraphQLUserListFetchResponse> callback) { | ||||
|         final Map<String, String> queryMap = new HashMap<>(); | ||||
|         queryMap.put("query_hash", "5f0b1f6281e72053cbc07909c8d154ae"); | ||||
|         queryMap.put("variables", "{\"comment_id\":\"" + commentId + "\"," + | ||||
|                 "\"first\":30," + | ||||
|                 "\"after\":\"" + (endCursor == null ? "" : endCursor) + "\"}"); | ||||
|         final Call<String> request = repository.fetch(queryMap); | ||||
|         request.enqueue(new Callback<String>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||
|                 final String rawBody = response.body(); | ||||
|                 if (rawBody == null) { | ||||
|                     Log.e(TAG, "Error occurred while fetching gql comment likes of " + commentId); | ||||
|                     callback.onSuccess(null); | ||||
|                     return; | ||||
|                 } | ||||
|                 try { | ||||
|                     final JSONObject body = new JSONObject(rawBody); | ||||
|                     final String status = body.getString("status"); | ||||
|                     final JSONObject data = body.getJSONObject("data").getJSONObject("comment").getJSONObject("edge_liked_by"); | ||||
|                     final JSONObject pageInfo = data.getJSONObject("page_info"); | ||||
|                     final String endCursor = pageInfo.getBoolean("has_next_page") ? pageInfo.getString("end_cursor") : null; | ||||
|                     final JSONArray users = data.getJSONArray("edges"); | ||||
|                     final int usersLen = users.length(); | ||||
|                     final List<User> userModels = new ArrayList<>(); | ||||
|                     for (int j = 0; j < usersLen; ++j) { | ||||
|                         final JSONObject userObject = users.getJSONObject(j).getJSONObject("node"); | ||||
|                         userModels.add(new User( | ||||
|                                 userObject.getLong("id"), | ||||
|                                 userObject.getString("username"), | ||||
|                                 userObject.optString("full_name"), | ||||
|                                 userObject.optBoolean("is_private"), | ||||
|                                 userObject.getString("profile_pic_url"), | ||||
|                                 userObject.optBoolean("is_verified") | ||||
|                         )); | ||||
|                         // userModels.add(new ProfileModel(userObject.optBoolean("is_private"), | ||||
|                         //                                 false, | ||||
|                         //                                 userObject.optBoolean("is_verified"), | ||||
|                         //                                 userObject.getString("id"), | ||||
|                         //                                 userObject.getString("username"), | ||||
|                         //                                 userObject.optString("full_name"), | ||||
|                         //                                 null, null, | ||||
|                         //                                 userObject.getString("profile_pic_url"), | ||||
|                         //                                 null, 0, 0, 0, false, false, false, false, false)); | ||||
|                     } | ||||
|                     callback.onSuccess(new GraphQLUserListFetchResponse(endCursor, status, userModels)); | ||||
|                 } catch (JSONException e) { | ||||
|                     Log.e(TAG, "onResponse", e); | ||||
|                     if (callback != null) { | ||||
|                         callback.onFailure(e); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onFailure(t); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public Call<String> fetchComments(final String shortCodeOrCommentId, | ||||
|                                       final boolean root, | ||||
|                                       final String cursor) { | ||||
|         final Map<String, String> queryMap = new HashMap<>(); | ||||
|         queryMap.put("query_hash", root ? "bc3296d1ce80a24b1b6e40b1e72903f5" : "51fdd02b67508306ad4484ff574a0b62"); | ||||
|         final Map<String, Object> variables = ImmutableMap.of( | ||||
|                 root ? "shortcode" : "comment_id", shortCodeOrCommentId, | ||||
|                 "first", 50, | ||||
|                 "after", cursor == null ? "" : cursor | ||||
|         ); | ||||
|         queryMap.put("variables", new JSONObject(variables).toString()); | ||||
|         return repository.fetch(queryMap); | ||||
|     } | ||||
| 
 | ||||
|     // TODO convert string response to a response class | ||||
|     public void fetchUser(final String username, | ||||
|                           final ServiceCallback<User> callback) { | ||||
|         final Call<String> request = repository.getUser(username); | ||||
|         request.enqueue(new Callback<String>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||
|                 final String rawBody = response.body(); | ||||
|                 if (rawBody == null) { | ||||
|                     Log.e(TAG, "Error occurred while fetching gql user of " + username); | ||||
|                     callback.onSuccess(null); | ||||
|                     return; | ||||
|                 } | ||||
|                 try { | ||||
|                     final JSONObject body = new JSONObject(rawBody); | ||||
|                     final JSONObject userJson = body.getJSONObject("graphql") | ||||
|                                                     .getJSONObject(Constants.EXTRAS_USER); | ||||
| 
 | ||||
|                     boolean isPrivate = userJson.getBoolean("is_private"); | ||||
|                     final long id = userJson.optLong(Constants.EXTRAS_ID, 0); | ||||
|                     final JSONObject timelineMedia = userJson.getJSONObject("edge_owner_to_timeline_media"); | ||||
|                     // if (timelineMedia.has("edges")) { | ||||
|                     //     final JSONArray edges = timelineMedia.getJSONArray("edges"); | ||||
|                     // } | ||||
| 
 | ||||
|                     String url = userJson.optString("external_url"); | ||||
|                     if (TextUtils.isEmpty(url)) url = null; | ||||
| 
 | ||||
|                     callback.onSuccess(new User( | ||||
|                             id, | ||||
|                             username, | ||||
|                             userJson.getString("full_name"), | ||||
|                             isPrivate, | ||||
|                             userJson.getString("profile_pic_url_hd"), | ||||
|                             null, | ||||
|                             new FriendshipStatus( | ||||
|                                     userJson.optBoolean("followed_by_viewer"), | ||||
|                                     userJson.optBoolean("follows_viewer"), | ||||
|                                     userJson.optBoolean("blocked_by_viewer"), | ||||
|                                     false, | ||||
|                                     isPrivate, | ||||
|                                     userJson.optBoolean("has_requested_viewer"), | ||||
|                                     userJson.optBoolean("requested_by_viewer"), | ||||
|                                     false, | ||||
|                                     userJson.optBoolean("restricted_by_viewer"), | ||||
|                                     false | ||||
|                             ), | ||||
|                             userJson.getBoolean("is_verified"), | ||||
|                             false, | ||||
|                             false, | ||||
|                             false, | ||||
|                             false, | ||||
|                             false, | ||||
|                             null, | ||||
|                             null, | ||||
|                             timelineMedia.getLong("count"), | ||||
|                             userJson.getJSONObject("edge_followed_by").getLong("count"), | ||||
|                             userJson.getJSONObject("edge_follow").getLong("count"), | ||||
|                             0, | ||||
|                             userJson.getString("biography"), | ||||
|                             url, | ||||
|                             0, | ||||
|                             null, | ||||
|                             null, | ||||
|                             null, | ||||
|                             null, | ||||
|                             null, | ||||
|                             null)); | ||||
|                 } catch (JSONException e) { | ||||
|                     Log.e(TAG, "onResponse", e); | ||||
|                     if (callback != null) { | ||||
|                         callback.onFailure(e); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onFailure(t); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     // TODO convert string response to a response class | ||||
|     public void fetchPost(final String shortcode, | ||||
|                           final ServiceCallback<Media> callback) { | ||||
|         final Call<String> request = repository.getPost(shortcode); | ||||
|         request.enqueue(new Callback<String>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||
|                 final String rawBody = response.body(); | ||||
|                 if (rawBody == null) { | ||||
|                     Log.e(TAG, "Error occurred while fetching gql post of " + shortcode); | ||||
|                     callback.onSuccess(null); | ||||
|                     return; | ||||
|                 } | ||||
|                 try { | ||||
|                     final JSONObject body = new JSONObject(rawBody); | ||||
|                     final JSONObject media = body.getJSONObject("graphql") | ||||
|                             .getJSONObject("shortcode_media"); | ||||
|                     callback.onSuccess(ResponseBodyUtils.parseGraphQLItem(media, null)); | ||||
|                 } catch (JSONException e) { | ||||
|                     Log.e(TAG, "onResponse", e); | ||||
|                     if (callback != null) { | ||||
|                         callback.onFailure(e); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onFailure(t); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     // TODO convert string response to a response class | ||||
|     public void fetchTag(final String tag, | ||||
|                          final ServiceCallback<Hashtag> callback) { | ||||
|         final Call<String> request = repository.getTag(tag); | ||||
|         request.enqueue(new Callback<String>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||
|                 final String rawBody = response.body(); | ||||
|                 if (rawBody == null) { | ||||
|                     Log.e(TAG, "Error occurred while fetching gql tag of " + tag); | ||||
|                     callback.onSuccess(null); | ||||
|                     return; | ||||
|                 } | ||||
|                 try { | ||||
|                     final JSONObject body = new JSONObject(rawBody) | ||||
|                             .getJSONObject("graphql") | ||||
|                             .getJSONObject(Constants.EXTRAS_HASHTAG); | ||||
|                     final JSONObject timelineMedia = body.getJSONObject("edge_hashtag_to_media"); | ||||
|                     callback.onSuccess(new Hashtag( | ||||
|                             body.getString(Constants.EXTRAS_ID), | ||||
|                             body.getString("name"), | ||||
|                             timelineMedia.getLong("count"), | ||||
|                             body.optBoolean("is_following") ? FollowingType.FOLLOWING : FollowingType.NOT_FOLLOWING, | ||||
|                             null)); | ||||
|                 } catch (JSONException e) { | ||||
|                     Log.e(TAG, "onResponse", e); | ||||
|                     if (callback != null) { | ||||
|                         callback.onFailure(e); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onFailure(t); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     // TODO convert string response to a response class | ||||
|     public void fetchLocation(final long locationId, | ||||
|                               final ServiceCallback<Location> callback) { | ||||
|         final Call<String> request = repository.getLocation(locationId); | ||||
|         request.enqueue(new Callback<String>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||
|                 final String rawBody = response.body(); | ||||
|                 if (rawBody == null) { | ||||
|                     Log.e(TAG, "Error occurred while fetching gql location of " + locationId); | ||||
|                     callback.onSuccess(null); | ||||
|                     return; | ||||
|                 } | ||||
|                 try { | ||||
|                     final JSONObject body = new JSONObject(rawBody) | ||||
|                             .getJSONObject("graphql") | ||||
|                             .getJSONObject(Constants.EXTRAS_LOCATION); | ||||
|                     final JSONObject timelineMedia = body.getJSONObject("edge_location_to_media"); | ||||
|                     final JSONObject address = new JSONObject(body.getString("address_json")); | ||||
|                     callback.onSuccess(new Location( | ||||
|                             body.getLong(Constants.EXTRAS_ID), | ||||
|                             body.getString("slug"), | ||||
|                             body.getString("name"), | ||||
|                             address.optString("street_address"), | ||||
|                             address.optString("city_name"), | ||||
|                             body.optDouble("lng", 0d), | ||||
|                             body.optDouble("lat", 0d) | ||||
|                     )); | ||||
|                 } catch (JSONException e) { | ||||
|                     Log.e(TAG, "onResponse", e); | ||||
|                     if (callback != null) { | ||||
|                         callback.onFailure(e); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onFailure(t); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,266 @@ | ||||
| package awais.instagrabber.webservices | ||||
| 
 | ||||
| import android.util.Log | ||||
| import awais.instagrabber.models.enums.FollowingType | ||||
| import awais.instagrabber.repositories.GraphQLRepository | ||||
| import awais.instagrabber.repositories.responses.* | ||||
| import awais.instagrabber.utils.Constants | ||||
| import awais.instagrabber.utils.ResponseBodyUtils | ||||
| import awais.instagrabber.utils.extensions.TAG | ||||
| import awais.instagrabber.webservices.RetrofitFactory.retrofitWeb | ||||
| import org.json.JSONException | ||||
| import org.json.JSONObject | ||||
| import java.util.* | ||||
| 
 | ||||
| object GraphQLService : BaseService() { | ||||
|     private val repository: GraphQLRepository = retrofitWeb.create(GraphQLRepository::class.java) | ||||
| 
 | ||||
|     // TODO convert string response to a response class | ||||
|     private suspend fun fetch( | ||||
|         queryHash: String, | ||||
|         variables: String, | ||||
|         arg1: String, | ||||
|         arg2: String, | ||||
|         backup: User?, | ||||
|     ): PostsFetchResponse { | ||||
|         val queryMap = mapOf( | ||||
|             "query_hash" to queryHash, | ||||
|             "variables" to variables, | ||||
|         ) | ||||
|         val response = repository.fetch(queryMap) | ||||
|         return parsePostResponse(response, arg1, arg2, backup) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun fetchLocationPosts( | ||||
|         locationId: Long, | ||||
|         maxId: String?, | ||||
|     ): PostsFetchResponse = fetch( | ||||
|         "36bd0f2bf5911908de389b8ceaa3be6d", | ||||
|         "{\"id\":\"" + locationId + "\"," + "\"first\":25," + "\"after\":\"" + (maxId ?: "") + "\"}", | ||||
|         Constants.EXTRAS_LOCATION, | ||||
|         "edge_location_to_media", | ||||
|         null | ||||
|     ) | ||||
| 
 | ||||
|     suspend fun fetchHashtagPosts( | ||||
|         tag: String, | ||||
|         maxId: String?, | ||||
|     ): PostsFetchResponse = fetch( | ||||
|         "9b498c08113f1e09617a1703c22b2f32", | ||||
|         "{\"tag_name\":\"" + tag + "\"," + "\"first\":25," + "\"after\":\"" + (maxId ?: "") + "\"}", | ||||
|         Constants.EXTRAS_HASHTAG, | ||||
|         "edge_hashtag_to_media", | ||||
|         null, | ||||
|     ) | ||||
| 
 | ||||
|     suspend fun fetchProfilePosts( | ||||
|         profileId: Long, | ||||
|         postsPerPage: Int, | ||||
|         maxId: String?, | ||||
|         backup: User?, | ||||
|     ): PostsFetchResponse = fetch( | ||||
|         "02e14f6a7812a876f7d133c9555b1151", | ||||
|         "{\"id\":\"" + profileId + "\"," + "\"first\":" + postsPerPage + "," + "\"after\":\"" + (maxId ?: "") + "\"}", | ||||
|         Constants.EXTRAS_USER, | ||||
|         "edge_owner_to_timeline_media", | ||||
|         backup, | ||||
|     ) | ||||
| 
 | ||||
|     suspend fun fetchTaggedPosts( | ||||
|         profileId: Long, | ||||
|         postsPerPage: Int, | ||||
|         maxId: String?, | ||||
|     ): PostsFetchResponse = fetch( | ||||
|         "31fe64d9463cbbe58319dced405c6206", | ||||
|         "{\"id\":\"" + profileId + "\"," + "\"first\":" + postsPerPage + "," + "\"after\":\"" + (maxId ?: "") + "\"}", | ||||
|         Constants.EXTRAS_USER, | ||||
|         "edge_user_to_photos_of_you", | ||||
|         null, | ||||
|     ) | ||||
| 
 | ||||
|     @Throws(JSONException::class) | ||||
|     private fun parsePostResponse( | ||||
|         response: String, | ||||
|         arg1: String, | ||||
|         arg2: String, | ||||
|         backup: User?, | ||||
|     ): PostsFetchResponse { | ||||
|         if (response.isBlank()) { | ||||
|             Log.e(TAG, "parseResponse: feed response body is empty") | ||||
|             return PostsFetchResponse(emptyList(), false, null) | ||||
|         } | ||||
|         return parseResponseBody(response, arg1, arg2, backup) | ||||
|     } | ||||
| 
 | ||||
|     @Throws(JSONException::class) | ||||
|     private fun parseResponseBody( | ||||
|         body: String, | ||||
|         arg1: String, | ||||
|         arg2: String, | ||||
|         backup: User?, | ||||
|     ): PostsFetchResponse { | ||||
|         val items: MutableList<Media> = ArrayList() | ||||
|         val timelineFeed = JSONObject(body) | ||||
|             .getJSONObject("data") | ||||
|             .getJSONObject(arg1) | ||||
|             .getJSONObject(arg2) | ||||
|         val endCursor: String? | ||||
|         val hasNextPage: Boolean | ||||
|         val pageInfo = timelineFeed.getJSONObject("page_info") | ||||
|         if (pageInfo.has("has_next_page")) { | ||||
|             hasNextPage = pageInfo.getBoolean("has_next_page") | ||||
|             endCursor = if (hasNextPage) pageInfo.getString("end_cursor") else null | ||||
|         } else { | ||||
|             hasNextPage = false | ||||
|             endCursor = null | ||||
|         } | ||||
|         val feedItems = timelineFeed.getJSONArray("edges") | ||||
|         for (i in 0 until feedItems.length()) { | ||||
|             val itemJson = feedItems.optJSONObject(i) ?: continue | ||||
|             val media = ResponseBodyUtils.parseGraphQLItem(itemJson, backup) | ||||
|             if (media != null) { | ||||
|                 items.add(media) | ||||
|             } | ||||
|         } | ||||
|         return PostsFetchResponse(items, hasNextPage, endCursor) | ||||
|     } | ||||
| 
 | ||||
|     // TODO convert string response to a response class | ||||
|     suspend fun fetchCommentLikers( | ||||
|         commentId: String, | ||||
|         endCursor: String?, | ||||
|     ): GraphQLUserListFetchResponse { | ||||
|         val queryMap = mapOf( | ||||
|             "query_hash" to "5f0b1f6281e72053cbc07909c8d154ae", | ||||
|             "variables" to "{\"comment_id\":\"" + commentId + "\"," + "\"first\":30," + "\"after\":\"" + (endCursor ?: "") + "\"}" | ||||
|         ) | ||||
|         val response = repository.fetch(queryMap) | ||||
|         val body = JSONObject(response) | ||||
|         val status = body.getString("status") | ||||
|         val data = body.getJSONObject("data").getJSONObject("comment").getJSONObject("edge_liked_by") | ||||
|         val pageInfo = data.getJSONObject("page_info") | ||||
|         val newEndCursor = if (pageInfo.getBoolean("has_next_page")) pageInfo.getString("end_cursor") else null | ||||
|         val users = data.getJSONArray("edges") | ||||
|         val usersLen = users.length() | ||||
|         val userModels: MutableList<User> = ArrayList() | ||||
|         for (j in 0 until usersLen) { | ||||
|             val userObject = users.getJSONObject(j).getJSONObject("node") | ||||
|             userModels.add(User( | ||||
|                 userObject.getLong("id"), | ||||
|                 userObject.getString("username"), | ||||
|                 userObject.optString("full_name"), | ||||
|                 userObject.optBoolean("is_private"), | ||||
|                 userObject.getString("profile_pic_url"), | ||||
|                 userObject.optBoolean("is_verified") | ||||
|             )) | ||||
|         } | ||||
|         return GraphQLUserListFetchResponse(newEndCursor, status, userModels) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun fetchComments( | ||||
|         shortCodeOrCommentId: String?, | ||||
|         root: Boolean, | ||||
|         cursor: String?, | ||||
|     ): String { | ||||
|         val variables = mapOf( | ||||
|             (if (root) "shortcode" else "comment_id") to shortCodeOrCommentId, | ||||
|             "first" to 50, | ||||
|             "after" to (cursor ?: "") | ||||
|         ) | ||||
|         val queryMap = mapOf( | ||||
|             "query_hash" to if (root) "bc3296d1ce80a24b1b6e40b1e72903f5" else "51fdd02b67508306ad4484ff574a0b62", | ||||
|             "variables" to JSONObject(variables).toString() | ||||
|         ) | ||||
|         return repository.fetch(queryMap) | ||||
|     } | ||||
| 
 | ||||
|     // TODO convert string response to a response class | ||||
|     suspend fun fetchUser( | ||||
|         username: String, | ||||
|     ): User { | ||||
|         val response = repository.getUser(username) | ||||
|         val body = JSONObject(response) | ||||
|         val userJson = body.getJSONObject("graphql").getJSONObject(Constants.EXTRAS_USER) | ||||
|         val isPrivate = userJson.getBoolean("is_private") | ||||
|         val id = userJson.optLong(Constants.EXTRAS_ID, 0) | ||||
|         val timelineMedia = userJson.getJSONObject("edge_owner_to_timeline_media") | ||||
|         // if (timelineMedia.has("edges")) { | ||||
|         //     final JSONArray edges = timelineMedia.getJSONArray("edges"); | ||||
|         // } | ||||
|         var url: String? = userJson.optString("external_url") | ||||
|         if (url.isNullOrBlank()) url = null | ||||
|         return User( | ||||
|             id, | ||||
|             username, | ||||
|             userJson.getString("full_name"), | ||||
|             isPrivate, | ||||
|             userJson.getString("profile_pic_url_hd"), | ||||
|             userJson.getBoolean("is_verified"), | ||||
|             friendshipStatus = FriendshipStatus( | ||||
|                 userJson.optBoolean("followed_by_viewer"), | ||||
|                 userJson.optBoolean("follows_viewer"), | ||||
|                 userJson.optBoolean("blocked_by_viewer"), | ||||
|                 false, | ||||
|                 isPrivate, | ||||
|                 userJson.optBoolean("has_requested_viewer"), | ||||
|                 userJson.optBoolean("requested_by_viewer"), | ||||
|                 false, | ||||
|                 userJson.optBoolean("restricted_by_viewer"), | ||||
|                 false | ||||
|             ), | ||||
|             mediaCount = timelineMedia.getLong("count"), | ||||
|             followerCount = userJson.getJSONObject("edge_followed_by").getLong("count"), | ||||
|             followingCount = userJson.getJSONObject("edge_follow").getLong("count"), | ||||
|             biography = userJson.getString("biography"), | ||||
|             externalUrl = url, | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     // TODO convert string response to a response class | ||||
|     suspend fun fetchPost( | ||||
|         shortcode: String, | ||||
|     ): Media { | ||||
|         val response = repository.getPost(shortcode) | ||||
|         val body = JSONObject(response) | ||||
|         val media = body.getJSONObject("graphql").getJSONObject("shortcode_media") | ||||
|         return ResponseBodyUtils.parseGraphQLItem(media, null) | ||||
|     } | ||||
| 
 | ||||
|     // TODO convert string response to a response class | ||||
|     suspend fun fetchTag( | ||||
|         tag: String, | ||||
|     ): Hashtag { | ||||
|         val response = repository.getTag(tag) | ||||
|         val body = JSONObject(response) | ||||
|             .getJSONObject("graphql") | ||||
|             .getJSONObject(Constants.EXTRAS_HASHTAG) | ||||
|         val timelineMedia = body.getJSONObject("edge_hashtag_to_media") | ||||
|         return Hashtag( | ||||
|             body.getString(Constants.EXTRAS_ID), | ||||
|             body.getString("name"), | ||||
|             timelineMedia.getLong("count"), | ||||
|             if (body.optBoolean("is_following")) FollowingType.FOLLOWING else FollowingType.NOT_FOLLOWING, | ||||
|             null) | ||||
|     } | ||||
| 
 | ||||
|     // TODO convert string response to a response class | ||||
|     suspend fun fetchLocation( | ||||
|         locationId: Long, | ||||
|     ): Location { | ||||
|         val response = repository.getLocation(locationId) | ||||
|         val body = JSONObject(response) | ||||
|             .getJSONObject("graphql") | ||||
|             .getJSONObject(Constants.EXTRAS_LOCATION) | ||||
|         // val timelineMedia = body.getJSONObject("edge_location_to_media") | ||||
|         val address = JSONObject(body.getString("address_json")) | ||||
|         return Location( | ||||
|             body.getLong(Constants.EXTRAS_ID), | ||||
|             body.getString("slug"), | ||||
|             body.getString("name"), | ||||
|             address.optString("street_address"), | ||||
|             address.optString("city_name"), | ||||
|             body.optDouble("lng", 0.0), | ||||
|             body.optDouble("lat", 0.0) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @ -12,11 +12,12 @@ import awais.instagrabber.utils.retryContextString | ||||
| import awais.instagrabber.webservices.RetrofitFactory.retrofit | ||||
| import org.json.JSONObject | ||||
| 
 | ||||
| class MediaService private constructor( | ||||
|     val deviceUuid: String, | ||||
|     val csrfToken: String, | ||||
|     val userId: Long, | ||||
| ) : BaseService() { | ||||
| object MediaService : BaseService() { | ||||
|     private val DELETABLE_ITEMS_TYPES = listOf( | ||||
|         MediaItemType.MEDIA_TYPE_IMAGE, | ||||
|         MediaItemType.MEDIA_TYPE_VIDEO, | ||||
|         MediaItemType.MEDIA_TYPE_SLIDER | ||||
|     ) | ||||
|     private val repository: MediaRepository = retrofit.create(MediaRepository::class.java) | ||||
| 
 | ||||
|     suspend fun fetch( | ||||
| @ -28,15 +29,38 @@ class MediaService private constructor( | ||||
|         } else response.items[0] | ||||
|     } | ||||
| 
 | ||||
|     suspend fun like(mediaId: String): Boolean = action(mediaId, "like", null) | ||||
|     suspend fun like( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         mediaId: String, | ||||
|     ): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "like", null) | ||||
| 
 | ||||
|     suspend fun unlike(mediaId: String): Boolean = action(mediaId, "unlike", null) | ||||
|     suspend fun unlike( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         mediaId: String, | ||||
|     ): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "unlike", null) | ||||
| 
 | ||||
|     suspend fun save(mediaId: String, collection: String?): Boolean = action(mediaId, "save", collection) | ||||
|     suspend fun save( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         mediaId: String, collection: String?, | ||||
|     ): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "save", collection) | ||||
| 
 | ||||
|     suspend fun unsave(mediaId: String): Boolean = action(mediaId, "unsave", null) | ||||
|     suspend fun unsave( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         mediaId: String, | ||||
|     ): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "unsave", null) | ||||
| 
 | ||||
|     private suspend fun action( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         mediaId: String, | ||||
|         action: String, | ||||
|         collection: String?, | ||||
| @ -60,6 +84,9 @@ class MediaService private constructor( | ||||
|     } | ||||
| 
 | ||||
|     suspend fun editCaption( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         postId: String, | ||||
|         newCaption: String, | ||||
|     ): Boolean { | ||||
| @ -99,7 +126,12 @@ class MediaService private constructor( | ||||
|         return jsonObject.optString("translation") | ||||
|     } | ||||
| 
 | ||||
|     suspend fun uploadFinish(options: UploadFinishOptions): String { | ||||
|     suspend fun uploadFinish( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         options: UploadFinishOptions, | ||||
|     ): String { | ||||
|         if (options.videoOptions != null) { | ||||
|             val videoOptions = options.videoOptions | ||||
|             if (videoOptions.clips.isEmpty()) { | ||||
| @ -124,6 +156,9 @@ class MediaService private constructor( | ||||
|     } | ||||
| 
 | ||||
|     suspend fun delete( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         postId: String, | ||||
|         type: MediaItemType, | ||||
|     ): String? { | ||||
| @ -144,26 +179,4 @@ class MediaService private constructor( | ||||
|         } | ||||
|         return repository.delete(postId, mediaType, signedForm) | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private val DELETABLE_ITEMS_TYPES = listOf( | ||||
|             MediaItemType.MEDIA_TYPE_IMAGE, | ||||
|             MediaItemType.MEDIA_TYPE_VIDEO, | ||||
|             MediaItemType.MEDIA_TYPE_SLIDER | ||||
|         ) | ||||
|         private lateinit var instance: MediaService | ||||
| 
 | ||||
|         @JvmStatic | ||||
|         fun getInstance(deviceUuid: String, csrfToken: String, userId: Long): MediaService { | ||||
|             if (!this::instance.isInitialized | ||||
|                 || instance.csrfToken != csrfToken | ||||
|                 || instance.deviceUuid != deviceUuid | ||||
|                 || instance.userId != userId | ||||
|             ) { | ||||
|                 instance = MediaService(deviceUuid, csrfToken, userId) | ||||
|             } | ||||
|             return instance | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -1,548 +0,0 @@ | ||||
| package awais.instagrabber.webservices; | ||||
| 
 | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import org.json.JSONArray; | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Objects; | ||||
| import java.util.UUID; | ||||
| 
 | ||||
| import awais.instagrabber.fragments.settings.PreferenceKeys; | ||||
| import awais.instagrabber.models.FeedStoryModel; | ||||
| import awais.instagrabber.models.HighlightModel; | ||||
| import awais.instagrabber.models.StoryModel; | ||||
| import awais.instagrabber.repositories.StoriesRepository; | ||||
| import awais.instagrabber.repositories.requests.StoryViewerOptions; | ||||
| import awais.instagrabber.repositories.responses.StoryStickerResponse; | ||||
| import awais.instagrabber.repositories.responses.User; | ||||
| import awais.instagrabber.utils.Constants; | ||||
| import awais.instagrabber.utils.ResponseBodyUtils; | ||||
| import awais.instagrabber.utils.TextUtils; | ||||
| import awais.instagrabber.utils.Utils; | ||||
| import retrofit2.Call; | ||||
| import retrofit2.Callback; | ||||
| import retrofit2.Response; | ||||
| 
 | ||||
| public class StoriesService extends BaseService { | ||||
|     private static final String TAG = "StoriesService"; | ||||
| 
 | ||||
|     private static StoriesService instance; | ||||
| 
 | ||||
|     private final StoriesRepository repository; | ||||
|     private final String csrfToken; | ||||
|     private final long userId; | ||||
|     private final String deviceUuid; | ||||
| 
 | ||||
|     private StoriesService(@NonNull final String csrfToken, | ||||
|                            final long userId, | ||||
|                            @NonNull final String deviceUuid) { | ||||
|         this.csrfToken = csrfToken; | ||||
|         this.userId = userId; | ||||
|         this.deviceUuid = deviceUuid; | ||||
|         repository = RetrofitFactory.INSTANCE | ||||
|                                     .getRetrofit() | ||||
|                                     .create(StoriesRepository.class); | ||||
|     } | ||||
| 
 | ||||
|     public String getCsrfToken() { | ||||
|         return csrfToken; | ||||
|     } | ||||
| 
 | ||||
|     public long getUserId() { | ||||
|         return userId; | ||||
|     } | ||||
| 
 | ||||
|     public String getDeviceUuid() { | ||||
|         return deviceUuid; | ||||
|     } | ||||
| 
 | ||||
|     public static StoriesService getInstance(final String csrfToken, | ||||
|                                              final long userId, | ||||
|                                              final String deviceUuid) { | ||||
|         if (instance == null | ||||
|                 || !Objects.equals(instance.getCsrfToken(), csrfToken) | ||||
|                 || !Objects.equals(instance.getUserId(), userId) | ||||
|                 || !Objects.equals(instance.getDeviceUuid(), deviceUuid)) { | ||||
|             instance = new StoriesService(csrfToken, userId, deviceUuid); | ||||
|         } | ||||
|         return instance; | ||||
|     } | ||||
| 
 | ||||
|     public void fetch(final long mediaId, | ||||
|                       final ServiceCallback<StoryModel> callback) { | ||||
|         final Call<String> request = repository.fetch(mediaId); | ||||
|         request.enqueue(new Callback<String>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<String> call, | ||||
|                                    @NonNull final Response<String> response) { | ||||
|                 if (callback == null) return; | ||||
|                 final String body = response.body(); | ||||
|                 if (body == null) { | ||||
|                     callback.onSuccess(null); | ||||
|                     return; | ||||
|                 } | ||||
|                 try { | ||||
|                     final JSONObject itemJson = new JSONObject(body).getJSONArray("items").getJSONObject(0); | ||||
|                     callback.onSuccess(ResponseBodyUtils.parseStoryItem(itemJson, false, null)); | ||||
|                 } catch (JSONException e) { | ||||
|                     callback.onFailure(e); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<String> call, | ||||
|                                   @NonNull final Throwable t) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onFailure(t); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void getFeedStories(final ServiceCallback<List<FeedStoryModel>> callback) { | ||||
|         final Call<String> response = repository.getFeedStories(); | ||||
|         response.enqueue(new Callback<String>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||
|                 final String body = response.body(); | ||||
|                 if (body == null) { | ||||
|                     Log.e(TAG, "getFeedStories: body is empty"); | ||||
|                     return; | ||||
|                 } | ||||
|                 parseStoriesBody(body, callback); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||
|                 callback.onFailure(t); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private void parseStoriesBody(final String body, final ServiceCallback<List<FeedStoryModel>> callback) { | ||||
|         try { | ||||
|             final List<FeedStoryModel> feedStoryModels = new ArrayList<>(); | ||||
|             final JSONArray feedStoriesReel = new JSONObject(body).getJSONArray("tray"); | ||||
|             for (int i = 0; i < feedStoriesReel.length(); ++i) { | ||||
|                 final JSONObject node = feedStoriesReel.getJSONObject(i); | ||||
|                 if (node.optBoolean("hide_from_feed_unit") && Utils.settingsHelper.getBoolean(PreferenceKeys.HIDE_MUTED_REELS)) continue; | ||||
|                 final JSONObject userJson = node.getJSONObject(node.has("user") ? "user" : "owner"); | ||||
|                 try { | ||||
|                     final User user = new User(userJson.getLong("pk"), | ||||
|                                                userJson.getString("username"), | ||||
|                                                userJson.optString("full_name"), | ||||
|                                                userJson.optBoolean("is_private"), | ||||
|                                                userJson.getString("profile_pic_url"), | ||||
|                                                userJson.optBoolean("is_verified") | ||||
|                     ); | ||||
|                     final long timestamp = node.getLong("latest_reel_media"); | ||||
|                     final boolean fullyRead = !node.isNull("seen") && node.getLong("seen") == timestamp; | ||||
|                     final JSONObject itemJson = node.has("items") ? node.getJSONArray("items").optJSONObject(0) : null; | ||||
|                     StoryModel firstStoryModel = null; | ||||
|                     if (itemJson != null) { | ||||
|                         firstStoryModel = ResponseBodyUtils.parseStoryItem(itemJson, false, null); | ||||
|                     } | ||||
|                     feedStoryModels.add(new FeedStoryModel( | ||||
|                             node.getString("id"), | ||||
|                             user, | ||||
|                             fullyRead, | ||||
|                             timestamp, | ||||
|                             firstStoryModel, | ||||
|                             node.getInt("media_count"), | ||||
|                             false, | ||||
|                             node.optBoolean("has_besties_media"))); | ||||
|                 } catch (Exception e) { | ||||
|                     Log.e(TAG, "parseStoriesBody: ", e); | ||||
|                 } // to cover promotional reels with non-long user pk's | ||||
|             } | ||||
|             final JSONArray broadcasts = new JSONObject(body).getJSONArray("broadcasts"); | ||||
|             for (int i = 0; i < broadcasts.length(); ++i) { | ||||
|                 final JSONObject node = broadcasts.getJSONObject(i); | ||||
|                 final JSONObject userJson = node.getJSONObject("broadcast_owner"); | ||||
|                 // final ProfileModel profileModel = new ProfileModel(false, false, false, | ||||
|                 //         userJson.getString("pk"), | ||||
|                 //         userJson.getString("username"), | ||||
|                 //         null, null, null, | ||||
|                 //         userJson.getString("profile_pic_url"), | ||||
|                 //         null, 0, 0, 0, false, false, false, false, false); | ||||
|                 final User user = new User(userJson.getLong("pk"), | ||||
|                                            userJson.getString("username"), | ||||
|                                            userJson.optString("full_name"), | ||||
|                                            userJson.optBoolean("is_private"), | ||||
|                                            userJson.getString("profile_pic_url"), | ||||
|                                            userJson.optBoolean("is_verified") | ||||
|                 ); | ||||
|                 feedStoryModels.add(new FeedStoryModel( | ||||
|                         node.getString("id"), | ||||
|                         user, | ||||
|                         false, | ||||
|                         node.getLong("published_time"), | ||||
|                         ResponseBodyUtils.parseBroadcastItem(node), | ||||
|                         1, | ||||
|                         true, | ||||
|                         false | ||||
|                 )); | ||||
|             } | ||||
|             callback.onSuccess(sort(feedStoryModels)); | ||||
|         } catch (JSONException e) { | ||||
|             Log.e(TAG, "Error parsing json", e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void fetchHighlights(final long profileId, | ||||
|                                 final ServiceCallback<List<HighlightModel>> callback) { | ||||
|         final Call<String> request = repository.fetchHighlights(profileId); | ||||
|         request.enqueue(new Callback<String>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||
|                 try { | ||||
|                     if (callback == null) { | ||||
|                         return; | ||||
|                     } | ||||
|                     final String body = response.body(); | ||||
|                     if (TextUtils.isEmpty(body)) { | ||||
|                         callback.onSuccess(null); | ||||
|                         return; | ||||
|                     } | ||||
|                     final JSONArray highlightsReel = new JSONObject(body).getJSONArray("tray"); | ||||
| 
 | ||||
|                     final int length = highlightsReel.length(); | ||||
|                     final List<HighlightModel> highlightModels = new ArrayList<>(); | ||||
| 
 | ||||
|                     for (int i = 0; i < length; ++i) { | ||||
|                         final JSONObject highlightNode = highlightsReel.getJSONObject(i); | ||||
|                         highlightModels.add(new HighlightModel( | ||||
|                                 highlightNode.getString("title"), | ||||
|                                 highlightNode.getString(Constants.EXTRAS_ID), | ||||
|                                 highlightNode.getJSONObject("cover_media") | ||||
|                                              .getJSONObject("cropped_image_version") | ||||
|                                              .getString("url"), | ||||
|                                 highlightNode.getLong("latest_reel_media"), | ||||
|                                 highlightNode.getInt("media_count") | ||||
|                         )); | ||||
|                     } | ||||
|                     callback.onSuccess(highlightModels); | ||||
|                 } catch (JSONException e) { | ||||
|                     Log.e(TAG, "onResponse", e); | ||||
|                     callback.onFailure(e); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onFailure(t); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void fetchArchive(final String maxId, | ||||
|                              final ServiceCallback<ArchiveFetchResponse> callback) { | ||||
|         final Map<String, String> form = new HashMap<>(); | ||||
|         form.put("include_suggested_highlights", "false"); | ||||
|         form.put("is_in_archive_home", "true"); | ||||
|         form.put("include_cover", "1"); | ||||
|         if (!TextUtils.isEmpty(maxId)) { | ||||
|             form.put("max_id", maxId); // NOT TESTED | ||||
|         } | ||||
|         final Call<String> request = repository.fetchArchive(form); | ||||
|         request.enqueue(new Callback<String>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||
|                 try { | ||||
|                     if (callback == null) { | ||||
|                         return; | ||||
|                     } | ||||
|                     final String body = response.body(); | ||||
|                     if (TextUtils.isEmpty(body)) { | ||||
|                         callback.onSuccess(null); | ||||
|                         return; | ||||
|                     } | ||||
|                     final JSONObject data = new JSONObject(body); | ||||
|                     final JSONArray highlightsReel = data.getJSONArray("items"); | ||||
| 
 | ||||
|                     final int length = highlightsReel.length(); | ||||
|                     final List<HighlightModel> highlightModels = new ArrayList<>(); | ||||
| 
 | ||||
|                     for (int i = 0; i < length; ++i) { | ||||
|                         final JSONObject highlightNode = highlightsReel.getJSONObject(i); | ||||
|                         highlightModels.add(new HighlightModel( | ||||
|                                 null, | ||||
|                                 highlightNode.getString(Constants.EXTRAS_ID), | ||||
|                                 highlightNode.getJSONObject("cover_image_version").getString("url"), | ||||
|                                 highlightNode.getLong("latest_reel_media"), | ||||
|                                 highlightNode.getInt("media_count") | ||||
|                         )); | ||||
|                     } | ||||
|                     callback.onSuccess(new ArchiveFetchResponse(highlightModels, | ||||
|                                                                 data.getBoolean("more_available"), | ||||
|                                                                 data.getString("max_id"))); | ||||
|                 } catch (JSONException e) { | ||||
|                     Log.e(TAG, "onResponse", e); | ||||
|                     callback.onFailure(e); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onFailure(t); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void getUserStory(final StoryViewerOptions options, | ||||
|                              final ServiceCallback<List<StoryModel>> callback) { | ||||
|         final String url = buildUrl(options); | ||||
|         final Call<String> userStoryCall = repository.getUserStory(url); | ||||
|         final boolean isLocOrHashtag = options.getType() == StoryViewerOptions.Type.LOCATION || options.getType() == StoryViewerOptions.Type.HASHTAG; | ||||
|         final boolean isHighlight = options.getType() == StoryViewerOptions.Type.HIGHLIGHT || options | ||||
|                 .getType() == StoryViewerOptions.Type.STORY_ARCHIVE; | ||||
|         userStoryCall.enqueue(new Callback<String>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||
|                 JSONObject data; | ||||
|                 try { | ||||
|                     final String body = response.body(); | ||||
|                     if (body == null) { | ||||
|                         Log.e(TAG, "body is null"); | ||||
|                         return; | ||||
|                     } | ||||
|                     data = new JSONObject(body); | ||||
| 
 | ||||
|                     if (!isHighlight) { | ||||
|                         data = data.optJSONObject((isLocOrHashtag) ? "story" : "reel"); | ||||
|                     } else { | ||||
|                         data = data.getJSONObject("reels").optJSONObject(options.getName()); | ||||
|                     } | ||||
| 
 | ||||
|                     String username = null; | ||||
|                     if (data != null | ||||
|                             // && localUsername == null | ||||
|                             && !isLocOrHashtag) { | ||||
|                         username = data.getJSONObject("user").getString("username"); | ||||
|                     } | ||||
| 
 | ||||
|                     JSONArray media; | ||||
|                     if (data != null | ||||
|                             && (media = data.optJSONArray("items")) != null | ||||
|                             && media.length() > 0 && media.optJSONObject(0) != null) { | ||||
|                         final int mediaLen = media.length(); | ||||
|                         final List<StoryModel> models = new ArrayList<>(); | ||||
|                         for (int i = 0; i < mediaLen; ++i) { | ||||
|                             data = media.getJSONObject(i); | ||||
|                             models.add(ResponseBodyUtils.parseStoryItem(data, isLocOrHashtag, username)); | ||||
|                         } | ||||
|                         callback.onSuccess(models); | ||||
|                     } else { | ||||
|                         callback.onSuccess(null); | ||||
|                     } | ||||
|                 } catch (JSONException e) { | ||||
|                     Log.e(TAG, "Error parsing string", e); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||
|                 callback.onFailure(t); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private void respondToSticker(final String storyId, | ||||
|                                   final String stickerId, | ||||
|                                   final String action, | ||||
|                                   final String arg1, | ||||
|                                   final String arg2, | ||||
|                                   final ServiceCallback<StoryStickerResponse> callback) { | ||||
|         final Map<String, Object> form = new HashMap<>(); | ||||
|         form.put("_csrftoken", csrfToken); | ||||
|         form.put("_uid", userId); | ||||
|         form.put("_uuid", deviceUuid); | ||||
|         form.put("mutation_token", UUID.randomUUID().toString()); | ||||
|         form.put("client_context", UUID.randomUUID().toString()); | ||||
|         form.put("radio_type", "wifi-none"); | ||||
|         form.put(arg1, arg2); | ||||
|         final Map<String, String> signedForm = Utils.sign(form); | ||||
|         final Call<StoryStickerResponse> request = | ||||
|                 repository.respondToSticker(storyId, stickerId, action, signedForm); | ||||
|         request.enqueue(new Callback<StoryStickerResponse>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<StoryStickerResponse> call, | ||||
|                                    @NonNull final Response<StoryStickerResponse> response) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onSuccess(response.body()); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<StoryStickerResponse> call, | ||||
|                                   @NonNull final Throwable t) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onFailure(t); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     // RespondAction.java | ||||
|     public void respondToQuestion(final String storyId, | ||||
|                                   final String stickerId, | ||||
|                                   final String answer, | ||||
|                                   final ServiceCallback<StoryStickerResponse> callback) { | ||||
|         respondToSticker(storyId, stickerId, "story_question_response", "response", answer, callback); | ||||
|     } | ||||
| 
 | ||||
|     // QuizAction.java | ||||
|     public void respondToQuiz(final String storyId, | ||||
|                               final String stickerId, | ||||
|                               final int answer, | ||||
|                               final ServiceCallback<StoryStickerResponse> callback) { | ||||
|         respondToSticker(storyId, stickerId, "story_quiz_answer", "answer", String.valueOf(answer), callback); | ||||
|     } | ||||
| 
 | ||||
|     // VoteAction.java | ||||
|     public void respondToPoll(final String storyId, | ||||
|                               final String stickerId, | ||||
|                               final int answer, | ||||
|                               final ServiceCallback<StoryStickerResponse> callback) { | ||||
|         respondToSticker(storyId, stickerId, "story_poll_vote", "vote", String.valueOf(answer), callback); | ||||
|     } | ||||
| 
 | ||||
|     public void respondToSlider(final String storyId, | ||||
|                                 final String stickerId, | ||||
|                                 final double answer, | ||||
|                                 final ServiceCallback<StoryStickerResponse> callback) { | ||||
|         respondToSticker(storyId, stickerId, "story_slider_vote", "vote", String.valueOf(answer), callback); | ||||
|     } | ||||
| 
 | ||||
|     public void seen(final String storyMediaId, | ||||
|                      final long takenAt, | ||||
|                      final long seenAt, | ||||
|                      final ServiceCallback<String> callback) { | ||||
|         final Map<String, Object> form = new HashMap<>(); | ||||
|         form.put("_csrftoken", csrfToken); | ||||
|         form.put("_uid", userId); | ||||
|         form.put("_uuid", deviceUuid); | ||||
|         form.put("container_module", "feed_timeline"); | ||||
|         final Map<String, Object> reelsForm = new HashMap<>(); | ||||
|         reelsForm.put(storyMediaId, Collections.singletonList(takenAt + "_" + seenAt)); | ||||
|         form.put("reels", reelsForm); | ||||
|         final Map<String, String> signedForm = Utils.sign(form); | ||||
|         final Map<String, String> queryMap = new HashMap<>(); | ||||
|         queryMap.put("reel", "1"); | ||||
|         queryMap.put("live_vod", "0"); | ||||
|         final Call<String> request = repository.seen(queryMap, signedForm); | ||||
|         request.enqueue(new Callback<String>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<String> call, | ||||
|                                    @NonNull final Response<String> response) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onSuccess(response.body()); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<String> call, | ||||
|                                   @NonNull final Throwable t) { | ||||
|                 if (callback != null) { | ||||
|                     callback.onFailure(t); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     private String buildUrl(@NonNull final StoryViewerOptions options) { | ||||
|         final StringBuilder builder = new StringBuilder(); | ||||
|         builder.append("https://i.instagram.com/api/v1/"); | ||||
|         final StoryViewerOptions.Type type = options.getType(); | ||||
|         String id = null; | ||||
|         switch (type) { | ||||
|             case HASHTAG: | ||||
|                 builder.append("tags/"); | ||||
|                 id = options.getName(); | ||||
|                 break; | ||||
|             case LOCATION: | ||||
|                 builder.append("locations/"); | ||||
|                 id = String.valueOf(options.getId()); | ||||
|                 break; | ||||
|             case USER: | ||||
|                 builder.append("feed/user/"); | ||||
|                 id = String.valueOf(options.getId()); | ||||
|                 break; | ||||
|             case HIGHLIGHT: | ||||
|             case STORY_ARCHIVE: | ||||
|                 builder.append("feed/reels_media/?user_ids="); | ||||
|                 id = options.getName(); | ||||
|                 break; | ||||
|             case STORY: | ||||
|                 break; | ||||
|             // case FEED_STORY_POSITION: | ||||
|             //     break; | ||||
|         } | ||||
|         if (id == null) { | ||||
|             return null; | ||||
|         } | ||||
|         builder.append(id); | ||||
|         if (type != StoryViewerOptions.Type.HIGHLIGHT && type != StoryViewerOptions.Type.STORY_ARCHIVE) { | ||||
|             builder.append("/story/"); | ||||
|         } | ||||
|         return builder.toString(); | ||||
|     } | ||||
| 
 | ||||
|     private List<FeedStoryModel> sort(final List<FeedStoryModel> list) { | ||||
|         final List<FeedStoryModel> listCopy = new ArrayList<>(list); | ||||
|         Collections.sort(listCopy, (o1, o2) -> { | ||||
|             int result; | ||||
|             switch (Utils.settingsHelper.getString(PreferenceKeys.STORY_SORT)) { | ||||
|                 case "1": | ||||
|                     result = Long.compare(o2.getTimestamp(), o1.getTimestamp()); | ||||
|                     break; | ||||
|                 case "2": | ||||
|                     result = Long.compare(o1.getTimestamp(), o2.getTimestamp()); | ||||
|                     break; | ||||
|                 default: | ||||
|                     result = 0; | ||||
|             } | ||||
|             return result; | ||||
|         }); | ||||
|         return listCopy; | ||||
|     } | ||||
| 
 | ||||
|     public static class ArchiveFetchResponse { | ||||
|         private final List<HighlightModel> archives; | ||||
|         private final boolean hasNextPage; | ||||
|         private final String nextCursor; | ||||
| 
 | ||||
|         public ArchiveFetchResponse(final List<HighlightModel> archives, final boolean hasNextPage, final String nextCursor) { | ||||
|             this.archives = archives; | ||||
|             this.hasNextPage = hasNextPage; | ||||
|             this.nextCursor = nextCursor; | ||||
|         } | ||||
| 
 | ||||
|         public List<HighlightModel> getResult() { | ||||
|             return archives; | ||||
|         } | ||||
| 
 | ||||
|         public boolean hasNextPage() { | ||||
|             return hasNextPage; | ||||
|         } | ||||
| 
 | ||||
|         public String getNextCursor() { | ||||
|             return nextCursor; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,309 @@ | ||||
| package awais.instagrabber.webservices | ||||
| 
 | ||||
| import android.util.Log | ||||
| import awais.instagrabber.fragments.settings.PreferenceKeys | ||||
| import awais.instagrabber.models.FeedStoryModel | ||||
| import awais.instagrabber.models.HighlightModel | ||||
| import awais.instagrabber.models.StoryModel | ||||
| import awais.instagrabber.repositories.StoriesRepository | ||||
| import awais.instagrabber.repositories.requests.StoryViewerOptions | ||||
| import awais.instagrabber.repositories.responses.StoryStickerResponse | ||||
| import awais.instagrabber.repositories.responses.User | ||||
| import awais.instagrabber.utils.Constants | ||||
| import awais.instagrabber.utils.ResponseBodyUtils | ||||
| import awais.instagrabber.utils.TextUtils.isEmpty | ||||
| import awais.instagrabber.utils.Utils | ||||
| import awais.instagrabber.utils.extensions.TAG | ||||
| import awais.instagrabber.webservices.RetrofitFactory.retrofit | ||||
| import org.json.JSONArray | ||||
| import org.json.JSONObject | ||||
| import java.util.* | ||||
| 
 | ||||
| object StoriesService : BaseService() { | ||||
|     private val repository: StoriesRepository = retrofit.create(StoriesRepository::class.java) | ||||
| 
 | ||||
|     suspend fun fetch(mediaId: Long): StoryModel { | ||||
|         val response = repository.fetch(mediaId) | ||||
|         val itemJson = JSONObject(response).getJSONArray("items").getJSONObject(0) | ||||
|         return ResponseBodyUtils.parseStoryItem(itemJson, false, null) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun getFeedStories(): List<FeedStoryModel> { | ||||
|         val response = repository.getFeedStories() | ||||
|         return parseStoriesBody(response) | ||||
|     } | ||||
| 
 | ||||
|     private fun parseStoriesBody(body: String): List<FeedStoryModel> { | ||||
|         val feedStoryModels: MutableList<FeedStoryModel> = ArrayList() | ||||
|         val feedStoriesReel = JSONObject(body).getJSONArray("tray") | ||||
|         for (i in 0 until feedStoriesReel.length()) { | ||||
|             val node = feedStoriesReel.getJSONObject(i) | ||||
|             if (node.optBoolean("hide_from_feed_unit") && Utils.settingsHelper.getBoolean(PreferenceKeys.HIDE_MUTED_REELS)) continue | ||||
|             val userJson = node.getJSONObject(if (node.has("user")) "user" else "owner") | ||||
|             try { | ||||
|                 val user = User(userJson.getLong("pk"), | ||||
|                     userJson.getString("username"), | ||||
|                     userJson.optString("full_name"), | ||||
|                     userJson.optBoolean("is_private"), | ||||
|                     userJson.getString("profile_pic_url"), | ||||
|                     userJson.optBoolean("is_verified") | ||||
|                 ) | ||||
|                 val timestamp = node.getLong("latest_reel_media") | ||||
|                 val fullyRead = !node.isNull("seen") && node.getLong("seen") == timestamp | ||||
|                 val itemJson = if (node.has("items")) node.getJSONArray("items").optJSONObject(0) else null | ||||
|                 var firstStoryModel: StoryModel? = null | ||||
|                 if (itemJson != null) { | ||||
|                     firstStoryModel = ResponseBodyUtils.parseStoryItem(itemJson, false, null) | ||||
|                 } | ||||
|                 feedStoryModels.add(FeedStoryModel( | ||||
|                     node.getString("id"), | ||||
|                     user, | ||||
|                     fullyRead, | ||||
|                     timestamp, | ||||
|                     firstStoryModel, | ||||
|                     node.getInt("media_count"), | ||||
|                     false, | ||||
|                     node.optBoolean("has_besties_media"))) | ||||
|             } catch (e: Exception) { | ||||
|                 Log.e(TAG, "parseStoriesBody: ", e) | ||||
|             } // to cover promotional reels with non-long user pk's | ||||
|         } | ||||
|         val broadcasts = JSONObject(body).getJSONArray("broadcasts") | ||||
|         for (i in 0 until broadcasts.length()) { | ||||
|             val node = broadcasts.getJSONObject(i) | ||||
|             val userJson = node.getJSONObject("broadcast_owner") | ||||
|             val user = User(userJson.getLong("pk"), | ||||
|                 userJson.getString("username"), | ||||
|                 userJson.optString("full_name"), | ||||
|                 userJson.optBoolean("is_private"), | ||||
|                 userJson.getString("profile_pic_url"), | ||||
|                 userJson.optBoolean("is_verified") | ||||
|             ) | ||||
|             feedStoryModels.add(FeedStoryModel( | ||||
|                 node.getString("id"), | ||||
|                 user, | ||||
|                 false, | ||||
|                 node.getLong("published_time"), | ||||
|                 ResponseBodyUtils.parseBroadcastItem(node), | ||||
|                 1, | ||||
|                 isLive = true, | ||||
|                 isBestie = false | ||||
|             )) | ||||
|         } | ||||
|         return sort(feedStoryModels) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun fetchHighlights(profileId: Long): List<HighlightModel> { | ||||
|         val response = repository.fetchHighlights(profileId) | ||||
|         val highlightsReel = JSONObject(response).getJSONArray("tray") | ||||
|         val length = highlightsReel.length() | ||||
|         val highlightModels: MutableList<HighlightModel> = ArrayList() | ||||
|         for (i in 0 until length) { | ||||
|             val highlightNode = highlightsReel.getJSONObject(i) | ||||
|             highlightModels.add(HighlightModel( | ||||
|                 highlightNode.getString("title"), | ||||
|                 highlightNode.getString(Constants.EXTRAS_ID), | ||||
|                 highlightNode.getJSONObject("cover_media") | ||||
|                     .getJSONObject("cropped_image_version") | ||||
|                     .getString("url"), | ||||
|                 highlightNode.getLong("latest_reel_media"), | ||||
|                 highlightNode.getInt("media_count") | ||||
|             )) | ||||
|         } | ||||
|         return highlightModels | ||||
|     } | ||||
| 
 | ||||
|     suspend fun fetchArchive(maxId: String): ArchiveFetchResponse { | ||||
|         val form = mutableMapOf( | ||||
|             "include_suggested_highlights" to "false", | ||||
|             "is_in_archive_home" to "true", | ||||
|             "include_cover" to "1", | ||||
|         ) | ||||
|         if (!isEmpty(maxId)) { | ||||
|             form["max_id"] = maxId // NOT TESTED | ||||
|         } | ||||
|         val response = repository.fetchArchive(form) | ||||
|         val data = JSONObject(response) | ||||
|         val highlightsReel = data.getJSONArray("items") | ||||
|         val length = highlightsReel.length() | ||||
|         val highlightModels: MutableList<HighlightModel> = ArrayList() | ||||
|         for (i in 0 until length) { | ||||
|             val highlightNode = highlightsReel.getJSONObject(i) | ||||
|             highlightModels.add(HighlightModel( | ||||
|                 null, | ||||
|                 highlightNode.getString(Constants.EXTRAS_ID), | ||||
|                 highlightNode.getJSONObject("cover_image_version").getString("url"), | ||||
|                 highlightNode.getLong("latest_reel_media"), | ||||
|                 highlightNode.getInt("media_count") | ||||
|             )) | ||||
|         } | ||||
|         return ArchiveFetchResponse(highlightModels, data.getBoolean("more_available"), data.getString("max_id")) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun getUserStory(options: StoryViewerOptions): List<StoryModel> { | ||||
|         val url = buildUrl(options) ?: return emptyList() | ||||
|         val response = repository.getUserStory(url) | ||||
|         val isLocOrHashtag = options.type == StoryViewerOptions.Type.LOCATION || options.type == StoryViewerOptions.Type.HASHTAG | ||||
|         val isHighlight = options.type == StoryViewerOptions.Type.HIGHLIGHT || options.type == StoryViewerOptions.Type.STORY_ARCHIVE | ||||
|         var data: JSONObject? = JSONObject(response) | ||||
|         data = if (!isHighlight) { | ||||
|             data?.optJSONObject(if (isLocOrHashtag) "story" else "reel") | ||||
|         } else { | ||||
|             data?.getJSONObject("reels")?.optJSONObject(options.name) | ||||
|         } | ||||
|         var username: String? = null | ||||
|         if (data != null && !isLocOrHashtag) { | ||||
|             username = data.getJSONObject("user").getString("username") | ||||
|         } | ||||
|         val media: JSONArray? = data?.optJSONArray("items") | ||||
|         return if (media?.length() ?: 0 > 0 && media?.optJSONObject(0) != null) { | ||||
|             val mediaLen = media.length() | ||||
|             val models: MutableList<StoryModel> = ArrayList() | ||||
|             for (i in 0 until mediaLen) { | ||||
|                 data = media.getJSONObject(i) | ||||
|                 models.add(ResponseBodyUtils.parseStoryItem(data, isLocOrHashtag, username)) | ||||
|             } | ||||
|             models | ||||
|         } else emptyList() | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun respondToSticker( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         storyId: String, | ||||
|         stickerId: String, | ||||
|         action: String, | ||||
|         arg1: String, | ||||
|         arg2: String, | ||||
|     ): StoryStickerResponse { | ||||
|         val form = mapOf( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uid" to userId, | ||||
|             "_uuid" to deviceUuid, | ||||
|             "mutation_token" to UUID.randomUUID().toString(), | ||||
|             "client_context" to UUID.randomUUID().toString(), | ||||
|             "radio_type" to "wifi-none", | ||||
|             arg1 to arg2, | ||||
|         ) | ||||
|         val signedForm = Utils.sign(form) | ||||
|         return repository.respondToSticker(storyId, stickerId, action, signedForm) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun respondToQuestion( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         storyId: String, | ||||
|         stickerId: String, | ||||
|         answer: String, | ||||
|     ): StoryStickerResponse = respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_question_response", "response", answer) | ||||
| 
 | ||||
|     suspend fun respondToQuiz( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         storyId: String, | ||||
|         stickerId: String, | ||||
|         answer: Int, | ||||
|     ): StoryStickerResponse { | ||||
|         return respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_quiz_answer", "answer", answer.toString()) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun respondToPoll( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         storyId: String, | ||||
|         stickerId: String, | ||||
|         answer: Int, | ||||
|     ): StoryStickerResponse = respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_poll_vote", "vote", answer.toString()) | ||||
| 
 | ||||
|     suspend fun respondToSlider( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         storyId: String, | ||||
|         stickerId: String, | ||||
|         answer: Double, | ||||
|     ): StoryStickerResponse = respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_slider_vote", "vote", answer.toString()) | ||||
| 
 | ||||
|     suspend fun seen( | ||||
|         csrfToken: String, | ||||
|         userId: Long, | ||||
|         deviceUuid: String, | ||||
|         storyMediaId: String, | ||||
|         takenAt: Long, | ||||
|         seenAt: Long, | ||||
|     ): String { | ||||
|         val reelsForm = mapOf(storyMediaId to listOf(takenAt.toString() + "_" + seenAt)) | ||||
|         val form = mutableMapOf( | ||||
|             "_csrftoken" to csrfToken, | ||||
|             "_uid" to userId, | ||||
|             "_uuid" to deviceUuid, | ||||
|             "container_module" to "feed_timeline", | ||||
|             "reels" to reelsForm, | ||||
|         ) | ||||
|         val signedForm = Utils.sign(form) | ||||
|         val queryMap = mapOf( | ||||
|             "reel" to "1", | ||||
|             "live_vod" to "0", | ||||
|         ) | ||||
|         return repository.seen(queryMap, signedForm) | ||||
|     } | ||||
| 
 | ||||
|     private fun buildUrl(options: StoryViewerOptions): String? { | ||||
|         val builder = StringBuilder() | ||||
|         builder.append("https://i.instagram.com/api/v1/") | ||||
|         val type = options.type | ||||
|         var id: String? = null | ||||
|         when (type) { | ||||
|             StoryViewerOptions.Type.HASHTAG -> { | ||||
|                 builder.append("tags/") | ||||
|                 id = options.name | ||||
|             } | ||||
|             StoryViewerOptions.Type.LOCATION -> { | ||||
|                 builder.append("locations/") | ||||
|                 id = options.id.toString() | ||||
|             } | ||||
|             StoryViewerOptions.Type.USER -> { | ||||
|                 builder.append("feed/user/") | ||||
|                 id = options.id.toString() | ||||
|             } | ||||
|             StoryViewerOptions.Type.HIGHLIGHT, StoryViewerOptions.Type.STORY_ARCHIVE -> { | ||||
|                 builder.append("feed/reels_media/?user_ids=") | ||||
|                 id = options.name | ||||
|             } | ||||
|             StoryViewerOptions.Type.STORY -> { | ||||
|             } | ||||
|             else -> { | ||||
|             } | ||||
|         } | ||||
|         if (id == null) { | ||||
|             return null | ||||
|         } | ||||
|         builder.append(id) | ||||
|         if (type != StoryViewerOptions.Type.HIGHLIGHT && type != StoryViewerOptions.Type.STORY_ARCHIVE) { | ||||
|             builder.append("/story/") | ||||
|         } | ||||
|         return builder.toString() | ||||
|     } | ||||
| 
 | ||||
|     private fun sort(list: List<FeedStoryModel>): List<FeedStoryModel> { | ||||
|         val listCopy = ArrayList(list) | ||||
|         listCopy.sortWith { o1, o2 -> | ||||
|             when (Utils.settingsHelper.getString(PreferenceKeys.STORY_SORT)) { | ||||
|                 "1" -> return@sortWith o2.timestamp.compareTo(o1.timestamp) | ||||
|                 "2" -> return@sortWith o1.timestamp.compareTo(o2.timestamp) | ||||
|                 else -> return@sortWith 0 | ||||
|             } | ||||
|         } | ||||
|         return listCopy | ||||
|     } | ||||
| 
 | ||||
|     class ArchiveFetchResponse(val result: List<HighlightModel>, val hasNextPage: Boolean, val nextCursor: String) { | ||||
|         fun hasNextPage(): Boolean { | ||||
|             return hasNextPage | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,101 +0,0 @@ | ||||
| package awais.instagrabber.webservices; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import java.util.TimeZone; | ||||
| 
 | ||||
| import awais.instagrabber.repositories.UserRepository; | ||||
| import awais.instagrabber.repositories.responses.FriendshipStatus; | ||||
| import awais.instagrabber.repositories.responses.User; | ||||
| import awais.instagrabber.repositories.responses.UserSearchResponse; | ||||
| import awais.instagrabber.repositories.responses.WrappedUser; | ||||
| import retrofit2.Call; | ||||
| import retrofit2.Callback; | ||||
| import retrofit2.Response; | ||||
| 
 | ||||
| public class UserService extends BaseService { | ||||
|     private static final String TAG = UserService.class.getSimpleName(); | ||||
| 
 | ||||
|     private final UserRepository repository; | ||||
| 
 | ||||
|     private static UserService instance; | ||||
| 
 | ||||
|     private UserService() { | ||||
|         repository = RetrofitFactory.INSTANCE | ||||
|                                     .getRetrofit() | ||||
|                                     .create(UserRepository.class); | ||||
|     } | ||||
| 
 | ||||
|     public static UserService getInstance() { | ||||
|         if (instance == null) { | ||||
|             instance = new UserService(); | ||||
|         } | ||||
|         return instance; | ||||
|     } | ||||
| 
 | ||||
|     public void getUserInfo(final long uid, final ServiceCallback<User> callback) { | ||||
|         final Call<WrappedUser> request = repository.getUserInfo(uid); | ||||
|         request.enqueue(new Callback<WrappedUser>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<WrappedUser> call, @NonNull final Response<WrappedUser> response) { | ||||
|                 final WrappedUser user = response.body(); | ||||
|                 if (user == null) { | ||||
|                     callback.onSuccess(null); | ||||
|                     return; | ||||
|                 } | ||||
|                 callback.onSuccess(user.getUser()); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<WrappedUser> call, @NonNull final Throwable t) { | ||||
|                 callback.onFailure(t); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void getUsernameInfo(final String username, final ServiceCallback<User> callback) { | ||||
|         final Call<WrappedUser> request = repository.getUsernameInfo(username); | ||||
|         request.enqueue(new Callback<WrappedUser>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<WrappedUser> call, @NonNull final Response<WrappedUser> response) { | ||||
|                 final WrappedUser user = response.body(); | ||||
|                 if (user == null) { | ||||
|                     callback.onFailure(null); | ||||
|                     return; | ||||
|                 } | ||||
|                 callback.onSuccess(user.getUser()); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<WrappedUser> call, @NonNull final Throwable t) { | ||||
|                 callback.onFailure(t); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void getUserFriendship(final long uid, final ServiceCallback<FriendshipStatus> callback) { | ||||
|         final Call<FriendshipStatus> request = repository.getUserFriendship(uid); | ||||
|         request.enqueue(new Callback<FriendshipStatus>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<FriendshipStatus> call, @NonNull final Response<FriendshipStatus> response) { | ||||
|                 final FriendshipStatus status = response.body(); | ||||
|                 if (status == null) { | ||||
|                     callback.onSuccess(null); | ||||
|                     return; | ||||
|                 } | ||||
|                 callback.onSuccess(status); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailure(@NonNull final Call<FriendshipStatus> call, @NonNull final Throwable t) { | ||||
|                 callback.onFailure(t); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public Call<UserSearchResponse> search(final String query) { | ||||
|         final float timezoneOffset = (float) TimeZone.getDefault().getRawOffset() / 1000; | ||||
|         return repository.search(timezoneOffset, query); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,29 @@ | ||||
| package awais.instagrabber.webservices | ||||
| 
 | ||||
| import awais.instagrabber.repositories.UserRepository | ||||
| import awais.instagrabber.repositories.responses.FriendshipStatus | ||||
| import awais.instagrabber.repositories.responses.User | ||||
| import awais.instagrabber.repositories.responses.UserSearchResponse | ||||
| import awais.instagrabber.webservices.RetrofitFactory.retrofit | ||||
| import java.util.* | ||||
| 
 | ||||
| object UserService : BaseService() { | ||||
|     private val repository: UserRepository = retrofit.create(UserRepository::class.java) | ||||
| 
 | ||||
|     suspend fun getUserInfo(uid: Long): User { | ||||
|         val response = repository.getUserInfo(uid) | ||||
|         return response.user | ||||
|     } | ||||
| 
 | ||||
|     suspend fun getUsernameInfo(username: String): User { | ||||
|         val response = repository.getUsernameInfo(username) | ||||
|         return response.user | ||||
|     } | ||||
| 
 | ||||
|     suspend fun getUserFriendship(uid: Long): FriendshipStatus = repository.getUserFriendship(uid) | ||||
| 
 | ||||
|     suspend fun search(query: String): UserSearchResponse { | ||||
|         val timezoneOffset = TimeZone.getDefault().rawOffset.toFloat() / 1000 | ||||
|         return repository.search(timezoneOffset, query) | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,18 @@ | ||||
| package awais.instagrabber.viewmodels | ||||
| 
 | ||||
| import androidx.lifecycle.SavedStateHandle | ||||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | ||||
| import org.junit.Test | ||||
| import org.junit.runner.RunWith | ||||
| 
 | ||||
| @RunWith(AndroidJUnit4::class) | ||||
| internal class ProfileFragmentViewModelTest { | ||||
|     @Test | ||||
|     fun testNoUsernameNoCurrentUser() { | ||||
|         val state = SavedStateHandle(mutableMapOf<String, Any>( | ||||
|             "username" to "" | ||||
|         )) | ||||
|         val viewModel = ProfileFragmentViewModel(state) | ||||
| 
 | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user