Browse Source

Merge branch 'master' of https://github.com/austinhuang0131/instagrabber

renovate/org.robolectric-robolectric-4.x
Austin Huang 4 years ago
parent
commit
7f7db43870
No known key found for this signature in database GPG Key ID: 84C23AA04587A91F
  1. 9
      app/build.gradle
  2. 43
      app/src/main/java/awais/instagrabber/activities/MainActivity.kt
  3. 16
      app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java
  4. 16
      app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java
  5. 18
      app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java
  6. 17
      app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java
  7. 34
      app/src/main/java/awais/instagrabber/db/dao/AccountDao.java
  8. 25
      app/src/main/java/awais/instagrabber/db/dao/AccountDao.kt
  9. 68
      app/src/main/java/awais/instagrabber/db/datasources/AccountDataSource.java
  10. 49
      app/src/main/java/awais/instagrabber/db/datasources/AccountDataSource.kt
  11. 131
      app/src/main/java/awais/instagrabber/db/repositories/AccountRepository.java
  12. 49
      app/src/main/java/awais/instagrabber/db/repositories/AccountRepository.kt
  13. 54
      app/src/main/java/awais/instagrabber/dialogs/AccountSwitcherDialogFragment.java
  14. 39
      app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java
  15. 130
      app/src/main/java/awais/instagrabber/fragments/FollowViewerFragment.java
  16. 55
      app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java
  17. 33
      app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java
  18. 61
      app/src/main/java/awais/instagrabber/fragments/LocationFragment.java
  19. 56
      app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java
  20. 42
      app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java
  21. 173
      app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java
  22. 4
      app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.kt
  23. 12
      app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java
  24. 30
      app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java
  25. 308
      app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java
  26. 135
      app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java
  27. 17
      app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.kt
  28. 19
      app/src/main/java/awais/instagrabber/managers/InboxManager.kt
  29. 209
      app/src/main/java/awais/instagrabber/managers/ThreadManager.kt
  30. 37
      app/src/main/java/awais/instagrabber/repositories/FriendshipRepository.java
  31. 36
      app/src/main/java/awais/instagrabber/repositories/FriendshipRepository.kt
  32. 25
      app/src/main/java/awais/instagrabber/repositories/GraphQLRepository.java
  33. 22
      app/src/main/java/awais/instagrabber/repositories/GraphQLRepository.kt
  34. 43
      app/src/main/java/awais/instagrabber/repositories/StoriesRepository.java
  35. 38
      app/src/main/java/awais/instagrabber/repositories/StoriesRepository.kt
  36. 25
      app/src/main/java/awais/instagrabber/repositories/UserRepository.java
  37. 25
      app/src/main/java/awais/instagrabber/repositories/UserRepository.kt
  38. 296
      app/src/main/java/awais/instagrabber/repositories/responses/User.java
  39. 38
      app/src/main/java/awais/instagrabber/repositories/responses/User.kt
  40. 10
      app/src/main/java/awais/instagrabber/utils/CookieUtils.kt
  41. 20
      app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java
  42. 21
      app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java
  43. 55
      app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java
  44. 4
      app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.kt
  45. 72
      app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt
  46. 21
      app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt
  47. 60
      app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java
  48. 199
      app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.kt
  49. 264
      app/src/main/java/awais/instagrabber/webservices/FriendshipService.java
  50. 155
      app/src/main/java/awais/instagrabber/webservices/FriendshipService.kt
  51. 483
      app/src/main/java/awais/instagrabber/webservices/GraphQLService.java
  52. 266
      app/src/main/java/awais/instagrabber/webservices/GraphQLService.kt
  53. 81
      app/src/main/java/awais/instagrabber/webservices/MediaService.kt
  54. 548
      app/src/main/java/awais/instagrabber/webservices/StoriesService.java
  55. 309
      app/src/main/java/awais/instagrabber/webservices/StoriesService.kt
  56. 101
      app/src/main/java/awais/instagrabber/webservices/UserService.java
  57. 29
      app/src/main/java/awais/instagrabber/webservices/UserService.kt
  58. 18
      app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt

9
app/build.gradle

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

43
app/src/main/java/awais/instagrabber/activities/MainActivity.kt

@ -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) } }
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 postCb: ServiceCallback<Media> = object : ServiceCallback<Media> {
override fun onSuccess(feedModel: Media?) {
if (feedModel != null) {
val currentNavControllerLiveData = currentNavControllerLiveData ?: return
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)
Log.e(TAG, "showPostView: ", e)
} finally {
withContext(Dispatchers.Main) {
alertDialog.dismiss()
}
}
} else {
graphQLService?.fetchPost(shortCode, postCb)
}
}

16
app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java

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

16
app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java

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

18
app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java

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

17
app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java

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

34
app/src/main/java/awais/instagrabber/db/dao/AccountDao.java

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

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

68
app/src/main/java/awais/instagrabber/db/datasources/AccountDataSource.java

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

49
app/src/main/java/awais/instagrabber/db/datasources/AccountDataSource.kt

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

131
app/src/main/java/awais/instagrabber/db/repositories/AccountRepository.java

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

49
app/src/main/java/awais/instagrabber/db/repositories/AccountRepository.kt

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

54
app/src/main/java/awais/instagrabber/dialogs/AccountSwitcherDialogFragment.java

@ -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) {
accountRepository.deleteAccount(
model,
CoroutineUtilsKt.getContinuation((unit, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
dismiss();
if (throwable != null) {
Log.e(TAG, "deleteAccount: ", throwable);
}
@Override
public void onDataNotAvailable() {
dismiss();
}
});
}), 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) {
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);
sortUserList(cookie, accounts);
adapter.submitList(accounts);
}
@Override
public void onDataNotAvailable() {}
});
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);

39
app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java

@ -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 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) return;
Toast.makeText(context, R.string.no_profile_pic_found, Toast.LENGTH_LONG).show();
if (context == null) {
dismiss();
return;
}
setupPhoto(url);
}
Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show();
dismiss();
return;
}
@Override
public void onFailure(final Throwable t) {
if (user != null) {
final String url = user.getHDProfilePicUrl();
if (TextUtils.isEmpty(url)) {
final Context context = getContext();
if (context == null) {
dismiss();
if (context == null) return;
Toast.makeText(context, R.string.no_profile_pic_found, Toast.LENGTH_LONG).show();
return;
}
Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show();
dismiss();
setupPhoto(url);
}
});
}), Dispatchers.getIO()));
} else setupPhoto(fallbackUrl);
}

130
app/src/main/java/awais/instagrabber/fragments/FollowViewerFragment.java

@ -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,
friendshipService.getList(
isFollowersList,
profileId,
endCursor,
isFollowersList ? followersFetchCb : followingFetchCb);
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,
friendshipService.getList(
!isFollowersList,
profileId,
null,
isFollowersList ? followingFetchCb : followersFetchCb);
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);

55
app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java

@ -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);
}
@Override
public void onFailure(final Throwable t) {
graphQLService.fetchPost(feedModel.getCode(), CoroutineUtilsKt.getContinuation((media, throwable) -> {
opening = false;
Log.e(TAG, "Error", t);
if (throwable != null) {
Log.e(TAG, "Error", throwable);
return;
}
});
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,9 +580,12 @@ 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) {
CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "Error", throwable);
storiesFetching = false;
return;
}
if (storyModels != null && !storyModels.isEmpty()) {
hashtagDetailsBinding.mainHashtagImage.setStoriesBorder(1);
hasStories = true;
@ -588,14 +593,8 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
hasStories = false;
}
storiesFetching = false;
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error", t);
storiesFetching = false;
}
});
}), Dispatchers.getIO())
);
}
private void setTitle() {

33
app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java

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

61
app/src/main/java/awais/instagrabber/fragments/LocationFragment.java

@ -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) {
graphQLService.fetchPost(
feedModel.getCode(),
CoroutineUtilsKt.getContinuation((media, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
opening = false;
Log.e(TAG, "Error", t);
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) {
CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "Error", throwable);
storiesFetching = false;
return;
}
if (storyModels != null && !storyModels.isEmpty()) {
locationDetailsBinding.mainLocationImage.setStoriesBorder(1);
hasStories = true;
}
storiesFetching = false;
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error", t);
storiesFetching = false;
}
});
}), Dispatchers.getIO())
);
}
}

56
app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java

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

42
app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java

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

173
app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java

@ -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) {
CoroutineUtilsKt.getContinuation(
(storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
sticking = false;
Log.e(TAG, "Error responding", throwable);
try {
poll.setMyChoice(w);
Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show();
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
@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();
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) {
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.answered_story, Toast.LENGTH_SHORT).show();
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
@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();
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) {
CoroutineUtilsKt.getContinuation(
(storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
sticking = false;
Log.e(TAG, "Error responding", throwable);
try {
quiz.setMyChoice(w);
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
@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();
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) {
CoroutineUtilsKt.getContinuation(
(storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
sticking = false;
Log.e(TAG, "Error responding", throwable);
try {
slider.setMyChoice(sliderValue);
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
@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();
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,9 +805,14 @@ 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) {
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) {
@ -799,14 +823,8 @@ public class StoryViewerFragment extends Fragment {
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);
}
});
}), 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(),
storiesService.seen(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId(),
currentStory.getTimestamp(),
System.currentTimeMillis() / 1000,
null);
CoroutineUtilsKt.getContinuation((s, throwable) -> {}, Dispatchers.getIO())
);
}
private void downloadStory() {

4
app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.kt

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

12
app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java

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

30
app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java

@ -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) {
storiesService.getFeedStories(
CoroutineUtilsKt.getContinuation((feedStoryModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "failed", throwable);
storiesFetching = false;
feedStoriesViewModel.getList().postValue(result);
feedStoriesAdapter.submitList(result);
if (storyListMenu != null) storyListMenu.setVisible(true);
updateSwipeRefreshState();
return;
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "failed", t);
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() {

308
app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java

@ -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();
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error while performing " + action, t);
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "Error while performing " + action, throwable);
return;
}
});
// 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);
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();
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, throwable1.getMessage(),
Toast.LENGTH_SHORT).show();
return;
}
graphQLService.fetchUser(usernameTemp, new ServiceCallback<User>() {
@Override
public void onSuccess(final User user) {
user.setFriendshipStatus(friendshipStatus);
profileModel = user;
setProfileDetails();
}), Dispatchers.getIO()
)
);
}), Dispatchers.getIO())
);
return;
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error fetching profile", t);
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();
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) {
}
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;
}
@Override
public void onDataNotAvailable() {
Log.e(TAG, "onDataNotAvailable: insert failed");
CoroutineUtilsKt.getContinuation((account, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "updateAccountInfo: ", throwable);
return;
}
});
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) {
CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "Error", throwable);
return;
}
if (storyModels != null && !storyModels.isEmpty()) {
profileDetailsBinding.mainProfileImage.setStoriesBorder(1);
hasStories = true;
}
}), 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;
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error", t);
}
});
storiesService.fetchHighlights(profileId,
new ServiceCallback<List<HighlightModel>>() {
@Override
public void onSuccess(final List<HighlightModel> result) {
if (result != null) {
if (highlightModels != null) {
profileDetailsBinding.highlightsList.setVisibility(View.VISIBLE);
highlightsViewModel.getList().postValue(result);
} else profileDetailsBinding.highlightsList.setVisibility(View.GONE);
}
@Override
public void onFailure(final Throwable t) {
//noinspection unchecked
highlightsViewModel.getList().postValue((List<HighlightModel>) highlightModels);
} else {
profileDetailsBinding.highlightsList.setVisibility(View.GONE);
Log.e(TAG, "Error", t);
}
});
}), 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)

135
app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java

@ -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,9 +93,19 @@ public class MorePreferencesFragment extends BasePreferencesFragment {
return true;
}));
}
accountRepository.getAllAccounts(new RepositoryCallback<List<Account>>() {
@Override
public void onSuccess(@NonNull final List<Account> accounts) {
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;
}));
}
return;
}
if (!isLoggedIn) {
if (accounts.size() > 0) {
final Context context1 = getContext();
@ -131,20 +136,24 @@ public class MorePreferencesFragment extends BasePreferencesFragment {
.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();
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);
}
@Override
public void onDataNotAvailable() {}
});
AppExecutors.INSTANCE
.getMainThread()
.execute(() -> ProcessPhoenix.triggerRebirth(context1), 200);
}),
Dispatchers.getIO()
)
);
})
.setNegativeButton(R.string.cancel, null)
.show();
@ -153,20 +162,8 @@ public class MorePreferencesFragment extends BasePreferencesFragment {
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;
}));
}
}
});
}), 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) {
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;
}
if (user != null) {
accountRepository.insertOrUpdateAccount(
uid,
result.getUsername(),
user.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();
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())
);
}
@Override
public void onDataNotAvailable() {
Log.e(TAG, "onDataNotAvailable: insert failed");
}
});
}
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error fetching user info", t);
}
});
}), 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) {
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();
});
}
@Override
public void onDataNotAvailable() {}
});
}), Dispatchers.getIO())
);
}
}
}

17
app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.kt

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

19
app/src/main/java/awais/instagrabber/managers/InboxManager.kt

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

209
app/src/main/java/awais/instagrabber/managers/ThreadManager.kt

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

37
app/src/main/java/awais/instagrabber/repositories/FriendshipRepository.java

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

36
app/src/main/java/awais/instagrabber/repositories/FriendshipRepository.kt

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

25
app/src/main/java/awais/instagrabber/repositories/GraphQLRepository.java

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

22
app/src/main/java/awais/instagrabber/repositories/GraphQLRepository.kt

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

43
app/src/main/java/awais/instagrabber/repositories/StoriesRepository.java

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

38
app/src/main/java/awais/instagrabber/repositories/StoriesRepository.kt

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

25
app/src/main/java/awais/instagrabber/repositories/UserRepository.java

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

25
app/src/main/java/awais/instagrabber/repositories/UserRepository.kt

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

296
app/src/main/java/awais/instagrabber/repositories/responses/User.java

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

38
app/src/main/java/awais/instagrabber/repositories/responses/User.kt

@ -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 ?: ""
}

10
app/src/main/java/awais/instagrabber/utils/CookieUtils.kt

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

20
app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java

@ -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,9 +397,13 @@ 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) {
accountRepository.getAllAccounts(
CoroutineUtilsKt.getContinuation((accounts, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "getCookies: ", throwable);
future.set(new JSONArray());
return;
}
final JSONArray jsonArray = new JSONArray();
try {
for (final Account cookie : accounts) {
@ -416,13 +421,8 @@ public final class ExportImportUtils {
}
}
future.set(jsonArray);
}
@Override
public void onDataNotAvailable() {
future.set(new JSONArray());
}
});
}), Dispatchers.getIO())
);
return future;
}

21
app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java

@ -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);
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "onFailure: ", t);
userService.getUserInfo(uid, CoroutineUtilsKt.getContinuation((user, throwable) -> {
if (throwable != null) {
Log.e(TAG, "onFailure: ", throwable);
return;
}
});
currentUser.postValue(user);
}, Dispatchers.getIO()));
}
}

55
app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java

@ -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,28 +194,28 @@ 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,
private Continuation<String> enqueueRequest(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) {
@SuppressWarnings("rawtypes") final ServiceCallback callback) {
return CoroutineUtilsKt.getContinuation((response, throwable) -> {
if (throwable != null) {
callback.onFailure(throwable);
return;
}
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(rawBody).getJSONObject("data")
final JSONObject body = root ? new JSONObject(response).getJSONObject("data")
.getJSONObject("shortcode_media")
.getJSONObject("edge_media_to_parent_comment")
: new JSONObject(rawBody).getJSONObject("data")
: new JSONObject(response).getJSONObject("data")
.getJSONObject("comment")
.getJSONObject("edge_threaded_comments");
final int count = body.optInt("count");
@ -224,20 +228,15 @@ public class CommentsViewerViewModel extends ViewModel {
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()));
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);
}
}
@Override
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) {
callback.onFailure(t);
}
});
}, Dispatchers.getIO());
}
@NonNull

4
app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.kt

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

72
app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt

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

21
app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt

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

60
app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java

@ -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,40 +168,13 @@ public class UserSearchViewModel extends ViewModel {
}
private void defaultUserSearch() {
searchRequest = userService.search(currentQuery);
//noinspection unchecked
handleRequest((Call<UserSearchResponse>) searchRequest);
}
private void rankedRecipientSearch() {
directMessagesService.rankedRecipients(
searchMode.getName(),
showGroups,
currentQuery,
CoroutineUtilsKt.getContinuation((response, throwable) -> {
userService.search(currentQuery, CoroutineUtilsKt.getContinuation((userSearchResponse, throwable) -> {
if (throwable != null) {
Log.e(TAG, "rankedRecipientSearch: ", throwable);
Log.e(TAG, "onFailure: ", throwable);
recipients.postValue(Resource.error(throwable.getMessage(), getCachedRecipients()));
return;
}
final List<RankedRecipient> list = response.getRankedRecipients();
if (list != null) {
recipients.postValue(Resource.success(mergeResponseWithCache(list)));
}
}, Dispatchers.getIO())
);
}
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;
@ -216,15 +187,26 @@ public class UserSearchViewModel extends ViewModel {
.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 void rankedRecipientSearch() {
directMessagesService.rankedRecipients(
searchMode.getName(),
showGroups,
currentQuery,
CoroutineUtilsKt.getContinuation((response, throwable) -> {
if (throwable != null) {
Log.e(TAG, "rankedRecipientSearch: ", throwable);
recipients.postValue(Resource.error(throwable.getMessage(), getCachedRecipients()));
return;
}
});
final List<RankedRecipient> list = response.getRankedRecipients();
if (list != null) {
recipients.postValue(Resource.success(mergeResponseWithCache(list)));
}
}, Dispatchers.getIO())
);
}
private List<RankedRecipient> mergeResponseWithCache(@NonNull final List<RankedRecipient> list) {

199
app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.kt

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

264
app/src/main/java/awais/instagrabber/webservices/FriendshipService.java

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

155
app/src/main/java/awais/instagrabber/webservices/FriendshipService.kt

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

483
app/src/main/java/awais/instagrabber/webservices/GraphQLService.java

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

266
app/src/main/java/awais/instagrabber/webservices/GraphQLService.kt

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

81
app/src/main/java/awais/instagrabber/webservices/MediaService.kt

@ -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 unlike(mediaId: String): Boolean = action(mediaId, "unlike", null)
suspend fun save(mediaId: String, collection: String?): Boolean = action(mediaId, "save", collection)
suspend fun like(
csrfToken: String,
userId: Long,
deviceUuid: String,
mediaId: String,
): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "like", null)
suspend fun unsave(mediaId: String): Boolean = action(mediaId, "unsave", null)
suspend fun unlike(
csrfToken: String,
userId: Long,
deviceUuid: String,
mediaId: String,
): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "unlike", null)
suspend fun save(
csrfToken: String,
userId: Long,
deviceUuid: String,
mediaId: String, collection: String?,
): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "save", collection)
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
}
}
}

548
app/src/main/java/awais/instagrabber/webservices/StoriesService.java

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

309
app/src/main/java/awais/instagrabber/webservices/StoriesService.kt

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

101
app/src/main/java/awais/instagrabber/webservices/UserService.java

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

29
app/src/main/java/awais/instagrabber/webservices/UserService.kt

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

18
app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt

@ -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…
Cancel
Save