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. 49
      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. 62
      app/src/main/java/awais/instagrabber/dialogs/AccountSwitcherDialogFragment.java
  14. 41
      app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java
  15. 138
      app/src/main/java/awais/instagrabber/fragments/FollowViewerFragment.java
  16. 65
      app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java
  17. 33
      app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java
  18. 69
      app/src/main/java/awais/instagrabber/fragments/LocationFragment.java
  19. 60
      app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java
  20. 44
      app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java
  21. 253
      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. 40
      app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java
  25. 332
      app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java
  26. 235
      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. 217
      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. 50
      app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java
  42. 21
      app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java
  43. 99
      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. 62
      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.md'
exclude 'META-INF/LICENSE-notice.md' exclude 'META-INF/LICENSE-notice.md'
} }
testOptions.unitTests {
includeAndroidResources = true
}
} }
configurations.all { configurations.all {
@ -190,6 +195,7 @@ dependencies {
def room_version = "2.3.0" def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-guava:$room_version" implementation "androidx.room:room-guava:$room_version"
implementation "androidx.room:room-ktx:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version"
// CameraX // CameraX
@ -231,6 +237,9 @@ dependencies {
githubImplementation 'io.sentry:sentry-android:4.3.0' githubImplementation 'io.sentry:sentry-android:4.3.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2' 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 'org.junit.jupiter:junit-jupiter:5.7.1'
androidTestImplementation 'androidx.test:core:1.3.0' androidTestImplementation 'androidx.test:core:1.3.0'

49
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.Resource
import awais.instagrabber.models.Tab import awais.instagrabber.models.Tab
import awais.instagrabber.models.enums.IntentModelType import awais.instagrabber.models.enums.IntentModelType
import awais.instagrabber.repositories.responses.Media
import awais.instagrabber.services.ActivityCheckerService import awais.instagrabber.services.ActivityCheckerService
import awais.instagrabber.services.DMSyncAlarmReceiver import awais.instagrabber.services.DMSyncAlarmReceiver
import awais.instagrabber.utils.* import awais.instagrabber.utils.*
@ -61,7 +60,6 @@ import awais.instagrabber.viewmodels.AppStateViewModel
import awais.instagrabber.viewmodels.DirectInboxViewModel import awais.instagrabber.viewmodels.DirectInboxViewModel
import awais.instagrabber.webservices.GraphQLService import awais.instagrabber.webservices.GraphQLService
import awais.instagrabber.webservices.MediaService import awais.instagrabber.webservices.MediaService
import awais.instagrabber.webservices.ServiceCallback
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior
import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.appbar.CollapsingToolbarLayout
@ -71,6 +69,7 @@ import com.google.common.collect.ImmutableList
import com.google.common.collect.Iterators import com.google.common.collect.Iterators
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.* import java.util.*
import java.util.stream.Collectors import java.util.stream.Collectors
@ -92,8 +91,6 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
var currentTabs: List<Tab> = emptyList() var currentTabs: List<Tab> = emptyList()
private set private set
private var showBottomViewDestinations: List<Int> = emptyList() private var showBottomViewDestinations: List<Int> = emptyList()
private var graphQLService: GraphQLService? = null
private var mediaService: MediaService? = null
private val serviceConnection: ServiceConnection = object : ServiceConnection { private val serviceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) { override fun onServiceConnected(name: ComponentName, service: IBinder) {
@ -637,42 +634,32 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
.setCancelable(false) .setCancelable(false)
.setView(R.layout.dialog_opening_post) .setView(R.layout.dialog_opening_post)
.create() .create()
if (graphQLService == null) graphQLService = GraphQLService.getInstance()
if (mediaService == null) {
mediaService = deviceUuid?.let { csrfToken?.let { it1 -> MediaService.getInstance(it, it1, userId) } }
}
val postCb: ServiceCallback<Media> = object : ServiceCallback<Media> {
override fun onSuccess(feedModel: Media?) {
if (feedModel != null) {
val currentNavControllerLiveData = currentNavControllerLiveData ?: return
alertDialog.show()
lifecycleScope.launch(Dispatchers.IO) {
try {
val media = if (isLoggedIn) MediaService.fetch(shortcodeToId(shortCode)) else GraphQLService.fetchPost(shortCode)
withContext(Dispatchers.Main) {
if (media == null) {
Toast.makeText(applicationContext, R.string.post_not_found, Toast.LENGTH_SHORT).show()
return@withContext
}
val currentNavControllerLiveData = currentNavControllerLiveData ?: return@withContext
val navController = currentNavControllerLiveData.value val navController = currentNavControllerLiveData.value
val bundle = Bundle() val bundle = Bundle()
bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, feedModel)
bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, media)
try { try {
navController?.navigate(R.id.action_global_post_view, bundle) navController?.navigate(R.id.action_global_post_view, bundle)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "showPostView: ", e) Log.e(TAG, "showPostView: ", e)
} }
} else Toast.makeText(applicationContext, R.string.post_not_found, Toast.LENGTH_SHORT).show()
alertDialog.dismiss()
}
override fun onFailure(t: Throwable) {
alertDialog.dismiss()
}
}
alertDialog.show()
if (isLoggedIn) {
lifecycleScope.launch(Dispatchers.IO) {
try {
val media = mediaService?.fetch(shortcodeToId(shortCode))
postCb.onSuccess(media)
} catch (e: Exception) {
postCb.onFailure(e)
}
} catch (e: Exception) {
Log.e(TAG, "showPostView: ", e)
} finally {
withContext(Dispatchers.Main) {
alertDialog.dismiss()
} }
} }
} else {
graphQLService?.fetchPost(shortCode, postCb)
} }
} }

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.Hashtag;
import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse; import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.TagsService; import awais.instagrabber.webservices.TagsService;
import kotlinx.coroutines.Dispatchers;
public class HashtagPostFetchService implements PostFetcher.PostFetchService { public class HashtagPostFetchService implements PostFetcher.PostFetchService {
private final TagsService tagsService; private final TagsService tagsService;
@ -23,7 +25,7 @@ public class HashtagPostFetchService implements PostFetcher.PostFetchService {
this.hashtagModel = hashtagModel; this.hashtagModel = hashtagModel;
this.isLoggedIn = isLoggedIn; this.isLoggedIn = isLoggedIn;
tagsService = isLoggedIn ? TagsService.getInstance() : null; tagsService = isLoggedIn ? TagsService.getInstance() : null;
graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE;
} }
@Override @Override
@ -48,7 +50,17 @@ public class HashtagPostFetchService implements PostFetcher.PostFetchService {
} }
}; };
if (isLoggedIn) tagsService.fetchPosts(hashtagModel.getName().toLowerCase(), nextMaxId, cb); 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 @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.Location;
import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse; import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.webservices.LocationService; import awais.instagrabber.webservices.LocationService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
public class LocationPostFetchService implements PostFetcher.PostFetchService { public class LocationPostFetchService implements PostFetcher.PostFetchService {
private final LocationService locationService; private final LocationService locationService;
@ -23,7 +25,7 @@ public class LocationPostFetchService implements PostFetcher.PostFetchService {
this.locationModel = locationModel; this.locationModel = locationModel;
this.isLoggedIn = isLoggedIn; this.isLoggedIn = isLoggedIn;
locationService = isLoggedIn ? LocationService.getInstance() : null; locationService = isLoggedIn ? LocationService.getInstance() : null;
graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE;
} }
@Override @Override
@ -48,7 +50,17 @@ public class LocationPostFetchService implements PostFetcher.PostFetchService {
} }
}; };
if (isLoggedIn) locationService.fetchPosts(locationModel.getPk(), nextMaxId, cb); 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 @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.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse; import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.webservices.ProfileService; import awais.instagrabber.webservices.ProfileService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
public class ProfilePostFetchService implements PostFetcher.PostFetchService { public class ProfilePostFetchService implements PostFetcher.PostFetchService {
private static final String TAG = "ProfilePostFetchService"; private static final String TAG = "ProfilePostFetchService";
@ -23,7 +25,7 @@ public class ProfilePostFetchService implements PostFetcher.PostFetchService {
public ProfilePostFetchService(final User profileModel, final boolean isLoggedIn) { public ProfilePostFetchService(final User profileModel, final boolean isLoggedIn) {
this.profileModel = profileModel; this.profileModel = profileModel;
this.isLoggedIn = isLoggedIn; this.isLoggedIn = isLoggedIn;
graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE;
profileService = isLoggedIn ? ProfileService.getInstance() : null; profileService = isLoggedIn ? ProfileService.getInstance() : null;
} }
@ -49,7 +51,19 @@ public class ProfilePostFetchService implements PostFetcher.PostFetchService {
} }
}; };
if (isLoggedIn) profileService.fetchPosts(profileModel.getPk(), nextMaxId, cb); 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 @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.models.enums.PostItemType;
import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse; import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.webservices.ProfileService; import awais.instagrabber.webservices.ProfileService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
public class SavedPostFetchService implements PostFetcher.PostFetchService { public class SavedPostFetchService implements PostFetcher.PostFetchService {
private final ProfileService profileService; private final ProfileService profileService;
@ -27,7 +29,7 @@ public class SavedPostFetchService implements PostFetcher.PostFetchService {
this.type = type; this.type = type;
this.isLoggedIn = isLoggedIn; this.isLoggedIn = isLoggedIn;
this.collectionId = collectionId; this.collectionId = collectionId;
graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE;
profileService = isLoggedIn ? ProfileService.getInstance() : null; profileService = isLoggedIn ? ProfileService.getInstance() : null;
} }
@ -58,7 +60,18 @@ public class SavedPostFetchService implements PostFetcher.PostFetchService {
break; break;
case TAGGED: case TAGGED:
if (isLoggedIn) profileService.fetchTagged(profileId, nextMaxId, callback); 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; break;
case COLLECTION: case COLLECTION:
case SAVED: 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
}
}
}

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

@ -3,6 +3,7 @@ package awais.instagrabber.dialogs;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -14,6 +15,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -23,30 +25,29 @@ import awais.instagrabber.databinding.DialogAccountSwitcherBinding;
import awais.instagrabber.db.datasources.AccountDataSource; import awais.instagrabber.db.datasources.AccountDataSource;
import awais.instagrabber.db.entities.Account; import awais.instagrabber.db.entities.Account;
import awais.instagrabber.db.repositories.AccountRepository; import awais.instagrabber.db.repositories.AccountRepository;
import awais.instagrabber.db.repositories.RepositoryCallback;
import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.ProcessPhoenix; import awais.instagrabber.utils.ProcessPhoenix;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import kotlinx.coroutines.Dispatchers;
import static awais.instagrabber.utils.Utils.settingsHelper; import static awais.instagrabber.utils.Utils.settingsHelper;
public class AccountSwitcherDialogFragment extends DialogFragment { public class AccountSwitcherDialogFragment extends DialogFragment {
private static final String TAG = AccountSwitcherDialogFragment.class.getSimpleName();
private AccountRepository accountRepository; private AccountRepository accountRepository;
private OnAddAccountClickListener onAddAccountClickListener; private OnAddAccountClickListener onAddAccountClickListener;
private DialogAccountSwitcherBinding binding; private DialogAccountSwitcherBinding binding;
public AccountSwitcherDialogFragment() {
accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(getContext()));
}
public AccountSwitcherDialogFragment() {}
public AccountSwitcherDialogFragment(final OnAddAccountClickListener onAddAccountClickListener) { public AccountSwitcherDialogFragment(final OnAddAccountClickListener onAddAccountClickListener) {
this.onAddAccountClickListener = onAddAccountClickListener; this.onAddAccountClickListener = onAddAccountClickListener;
accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(getContext()));
} }
private final AccountSwitcherAdapter.OnAccountClickListener accountClickListener = (model, isCurrent) -> { 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())) .setMessage(getString(R.string.quick_access_confirm_delete, model.getUsername()))
.setPositiveButton(R.string.yes, (dialog, which) -> { .setPositiveButton(R.string.yes, (dialog, which) -> {
if (accountRepository == null) return; if (accountRepository == null) return;
accountRepository.deleteAccount(model, new RepositoryCallback<Void>() {
@Override
public void onSuccess(final Void result) {
dismiss();
}
@Override
public void onDataNotAvailable() {
dismiss();
}
});
accountRepository.deleteAccount(
model,
CoroutineUtilsKt.getContinuation((unit, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
dismiss();
if (throwable != null) {
Log.e(TAG, "deleteAccount: ", throwable);
}
}), Dispatchers.getIO())
);
}) })
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show(); .show();
@ -113,6 +112,12 @@ public class AccountSwitcherDialogFragment extends DialogFragment {
init(); init();
} }
@Override
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(context));
}
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
@ -129,18 +134,19 @@ public class AccountSwitcherDialogFragment extends DialogFragment {
final AccountSwitcherAdapter adapter = new AccountSwitcherAdapter(accountClickListener, accountLongClickListener); final AccountSwitcherAdapter adapter = new AccountSwitcherAdapter(accountClickListener, accountLongClickListener);
binding.accounts.setAdapter(adapter); binding.accounts.setAdapter(adapter);
if (accountRepository == null) return; if (accountRepository == null) return;
accountRepository.getAllAccounts(new RepositoryCallback<List<Account>>() {
@Override
public void onSuccess(final List<Account> accounts) {
if (accounts == null) return;
final String cookie = settingsHelper.getString(Constants.COOKIE);
sortUserList(cookie, accounts);
adapter.submitList(accounts);
}
@Override
public void onDataNotAvailable() {}
});
accountRepository.getAllAccounts(
CoroutineUtilsKt.getContinuation((accounts, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "init: ", throwable);
return;
}
if (accounts == null) return;
final String cookie = settingsHelper.getString(Constants.COOKIE);
final List<Account> copy = new ArrayList<>(accounts);
sortUserList(cookie, copy);
adapter.submitList(copy);
}), Dispatchers.getIO())
);
binding.addAccountBtn.setOnClickListener(v -> { binding.addAccountBtn.setOnClickListener(v -> {
if (onAddAccountClickListener == null) return; if (onAddAccountClickListener == null) return;
onAddAccountClickListener.onAddAccountClick(this); onAddAccountClickListener.onAddAccountClick(this);

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

@ -28,13 +28,14 @@ import java.io.File;
import awais.instagrabber.R; import awais.instagrabber.R;
import awais.instagrabber.databinding.DialogProfilepicBinding; 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.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.UserService; import awais.instagrabber.webservices.UserService;
import kotlinx.coroutines.Dispatchers;
import static awais.instagrabber.utils.Utils.settingsHelper; import static awais.instagrabber.utils.Utils.settingsHelper;
@ -129,33 +130,29 @@ public class ProfilePicDialogFragment extends DialogFragment {
private void fetchAvatar() { private void fetchAvatar() {
if (isLoggedIn) { if (isLoggedIn) {
final UserService userService = UserService.getInstance();
userService.getUserInfo(id, new ServiceCallback<User>() {
@Override
public void onSuccess(final User result) {
if (result != null) {
final String url = result.getHDProfilePicUrl();
if (url == null) {
final Context context = getContext();
if (context == null) return;
Toast.makeText(context, R.string.no_profile_pic_found, Toast.LENGTH_LONG).show();
return;
}
setupPhoto(url);
}
}
@Override
public void onFailure(final Throwable t) {
final UserService userService = UserService.INSTANCE;
userService.getUserInfo(id, CoroutineUtilsKt.getContinuation((user, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
final Context context = getContext(); final Context context = getContext();
if (context == null) { if (context == null) {
dismiss(); dismiss();
return; return;
} }
Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show();
Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show();
dismiss(); dismiss();
return;
}
if (user != null) {
final String url = user.getHDProfilePicUrl();
if (TextUtils.isEmpty(url)) {
final Context context = getContext();
if (context == null) return;
Toast.makeText(context, R.string.no_profile_pic_found, Toast.LENGTH_LONG).show();
return;
}
setupPhoto(url);
} }
});
}), Dispatchers.getIO()));
} else setupPhoto(fallbackUrl); } else setupPhoto(fallbackUrl);
} }

138
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.databinding.FragmentFollowersViewerBinding;
import awais.instagrabber.models.FollowModel; import awais.instagrabber.models.FollowModel;
import awais.instagrabber.repositories.responses.FriendshipListFetchResponse; import awais.instagrabber.repositories.responses.FriendshipListFetchResponse;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.webservices.FriendshipService; import awais.instagrabber.webservices.FriendshipService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
import thoughtbot.expandableadapter.ExpandableGroup; import thoughtbot.expandableadapter.ExpandableGroup;
public final class FollowViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { 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 (!isFollowersList) followModels.addAll(result.getItems());
if (result.isMoreAvailable()) { if (result.isMoreAvailable()) {
endCursor = result.getNextMaxId(); 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) { } else if (followersModels.size() == 0) {
if (!isFollowersList) moreAvailable = false; 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 { } else {
if (!isFollowersList) moreAvailable = false; if (!isFollowersList) moreAvailable = false;
showCompare(); showCompare();
@ -84,8 +109,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
try { try {
binding.swipeRefreshLayout.setRefreshing(false); binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
}
catch(Throwable e) {}
} catch (Throwable ignored) {}
Log.e(TAG, "Error fetching list (double, following)", t); 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 (isFollowersList) followModels.addAll(result.getItems());
if (result.isMoreAvailable()) { if (result.isMoreAvailable()) {
endCursor = result.getNextMaxId(); 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) { } else if (followingModels.size() == 0) {
if (isFollowersList) moreAvailable = false; 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 { } else {
if (isFollowersList) moreAvailable = false; if (isFollowersList) moreAvailable = false;
showCompare(); showCompare();
@ -113,8 +159,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
try { try {
binding.swipeRefreshLayout.setRefreshing(false); binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
}
catch(Throwable e) {}
} catch (Throwable ignored) {}
Log.e(TAG, "Error fetching list (double, follower)", t); Log.e(TAG, "Error fetching list (double, follower)", t);
} }
}; };
@ -122,7 +167,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
@Override @Override
public void onCreate(@Nullable final Bundle savedInstanceState) { public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
friendshipService = FriendshipService.getInstance(null, null, 0);
friendshipService = FriendshipService.INSTANCE;
fragmentActivity = (AppCompatActivity) getActivity(); fragmentActivity = (AppCompatActivity) getActivity();
setHasOptionsMenu(true); setHasOptionsMenu(true);
} }
@ -235,8 +280,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
try { try {
binding.swipeRefreshLayout.setRefreshing(false); binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
}
catch(Throwable e) {}
} catch (Throwable ignored) {}
Log.e(TAG, "Error fetching list (single)", t); 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) { if (!TextUtils.isEmpty(endCursor) && !searching) {
binding.swipeRefreshLayout.setRefreshing(true); binding.swipeRefreshLayout.setRefreshing(true);
layoutManager.setStackFromEnd(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; endCursor = null;
} }
}); });
@ -253,7 +308,18 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
binding.rvFollow.setLayoutManager(layoutManager); binding.rvFollow.setLayoutManager(layoutManager);
if (moreAvailable) { if (moreAvailable) {
binding.swipeRefreshLayout.setRefreshing(true); 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 { } else {
refreshAdapter(followModels, null, null, null); refreshAdapter(followModels, null, null, null);
layoutManager.scrollToPosition(0); layoutManager.scrollToPosition(0);
@ -269,17 +335,34 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
if (moreAvailable) { if (moreAvailable) {
binding.swipeRefreshLayout.setRefreshing(true); binding.swipeRefreshLayout.setRefreshing(true);
Toast.makeText(getContext(), R.string.follower_start_compare, Toast.LENGTH_LONG).show(); Toast.makeText(getContext(), R.string.follower_start_compare, Toast.LENGTH_LONG).show();
friendshipService.getList(isFollowersList,
profileId,
endCursor,
isFollowersList ? followersFetchCb : followingFetchCb);
friendshipService.getList(
isFollowersList,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
final ServiceCallback<FriendshipListFetchResponse> callback = isFollowersList ? followersFetchCb : followingFetchCb;
if (throwable != null) {
callback.onFailure(throwable);
return;
}
callback.onSuccess(response);
}), Dispatchers.getIO())
);
} else if (followersModels.size() == 0 || followingModels.size() == 0) { } else if (followersModels.size() == 0 || followingModels.size() == 0) {
binding.swipeRefreshLayout.setRefreshing(true); binding.swipeRefreshLayout.setRefreshing(true);
Toast.makeText(getContext(), R.string.follower_start_compare, Toast.LENGTH_LONG).show(); Toast.makeText(getContext(), R.string.follower_start_compare, Toast.LENGTH_LONG).show();
friendshipService.getList(!isFollowersList,
profileId,
null,
isFollowersList ? followingFetchCb : followersFetchCb);
friendshipService.getList(
!isFollowersList,
profileId,
null,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
final ServiceCallback<FriendshipListFetchResponse> callback = isFollowersList ? followingFetchCb : followersFetchCb;
if (throwable != null) {
callback.onFailure(throwable);
return;
}
callback.onSuccess(response);
}), Dispatchers.getIO()));
} else showCompare(); } else showCompare();
} }
@ -337,10 +420,10 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
final Context context = getContext(); final Context context = getContext();
if (loading) Toast.makeText(context, R.string.follower_wait_to_load, Toast.LENGTH_LONG).show(); if (loading) Toast.makeText(context, R.string.follower_wait_to_load, Toast.LENGTH_LONG).show();
else if (isCompare) { else if (isCompare) {
isCompare = !isCompare;
isCompare = false;
listFollows(); listFollows();
} else { } else {
isCompare = !isCompare;
isCompare = true;
listCompare(); listCompare();
} }
return true; return true;
@ -354,16 +437,15 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
final ArrayList<ExpandableGroup> groups = new ArrayList<>(1); final ArrayList<ExpandableGroup> groups = new ArrayList<>(1);
if (isCompare && followingModels != null && followersModels != null && allFollowing != null) { 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)); 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)); 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)); groups.add(new ExpandableGroup(resources.getString(R.string.followers_both_following), allFollowing));
} else if (followModels != null) { } else if (followModels != null) {
groups.add(new ExpandableGroup(type, followModels)); groups.add(new ExpandableGroup(type, followModels));
}
else return;
} else return;
adapter = new FollowAdapter(clickListener, groups); adapter = new FollowAdapter(clickListener, groups);
adapter.toggleGroup(0); adapter.toggleGroup(0);
binding.rvFollow.setAdapter(adapter); binding.rvFollow.setAdapter(adapter);

65
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 com.google.common.collect.ImmutableList;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
import java.util.Set; import java.util.Set;
import awais.instagrabber.R; import awais.instagrabber.R;
@ -55,7 +54,6 @@ import awais.instagrabber.db.repositories.FavoriteRepository;
import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.db.repositories.RepositoryCallback;
import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment;
import awais.instagrabber.models.PostsLayoutPreferences; import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.StoryModel;
import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.models.enums.FollowingType; import awais.instagrabber.models.enums.FollowingType;
import awais.instagrabber.repositories.requests.StoryViewerOptions; 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.Location;
import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
@ -72,6 +72,7 @@ import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.StoriesService; import awais.instagrabber.webservices.StoriesService;
import awais.instagrabber.webservices.TagsService; import awais.instagrabber.webservices.TagsService;
import kotlinx.coroutines.Dispatchers;
import static androidx.core.content.PermissionChecker.checkSelfPermission; import static androidx.core.content.PermissionChecker.checkSelfPermission;
import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; 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())) { if (TextUtils.isEmpty(user.getUsername())) {
// this only happens for anons // this only happens for anons
opening = true; opening = true;
graphQLService.fetchPost(feedModel.getCode(), new ServiceCallback<Media>() {
@Override
public void onSuccess(final Media newFeedModel) {
opening = false;
if (newFeedModel == null) return;
openPostDialog(newFeedModel, profilePicView, mainPostImage, position);
}
@Override
public void onFailure(final Throwable t) {
opening = false;
Log.e(TAG, "Error", t);
graphQLService.fetchPost(feedModel.getCode(), CoroutineUtilsKt.getContinuation((media, throwable) -> {
opening = false;
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; return;
} }
opening = true; opening = true;
@ -303,8 +299,8 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
final String cookie = settingsHelper.getString(Constants.COOKIE); final String cookie = settingsHelper.getString(Constants.COOKIE);
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0;
tagsService = isLoggedIn ? TagsService.getInstance() : null; 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); setHasOptionsMenu(true);
} }
@ -385,7 +381,13 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
private void fetchHashtagModel() { private void fetchHashtagModel() {
binding.swipeRefreshLayout.setRefreshing(true); binding.swipeRefreshLayout.setRefreshing(true);
if (isLoggedIn) tagsService.fetch(hashtag, cb); 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() { private void setupPosts() {
@ -578,24 +580,21 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
storiesFetching = true; storiesFetching = true;
storiesService.getUserStory( storiesService.getUserStory(
StoryViewerOptions.forHashtag(hashtagModel.getName()), StoryViewerOptions.forHashtag(hashtagModel.getName()),
new ServiceCallback<List<StoryModel>>() {
@Override
public void onSuccess(final List<StoryModel> storyModels) {
if (storyModels != null && !storyModels.isEmpty()) {
hashtagDetailsBinding.mainHashtagImage.setStoriesBorder(1);
hasStories = true;
} else {
hasStories = false;
}
CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "Error", throwable);
storiesFetching = false; storiesFetching = false;
return;
} }
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error", t);
storiesFetching = false;
if (storyModels != null && !storyModels.isEmpty()) {
hashtagDetailsBinding.mainHashtagImage.setStoriesBorder(1);
hasStories = true;
} else {
hasStories = false;
} }
});
storiesFetching = false;
}), Dispatchers.getIO())
);
} }
private void setTitle() { 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 String cookie = settingsHelper.getString(Constants.COOKIE);
final long userId = CookieUtils.getUserIdFromCookie(cookie); final long userId = CookieUtils.getUserIdFromCookie(cookie);
isLoggedIn = !TextUtils.isEmpty(cookie) && userId != 0; 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); final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
if (csrfToken == null) return; 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); // setHasOptionsMenu(true);
} }
@ -135,7 +135,17 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme
public void onRefresh() { public void onRefresh() {
if (isComment && !isLoggedIn) { if (isComment && !isLoggedIn) {
lazyLoader.resetState(); 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 { } else {
mediaService.fetchLikes( mediaService.fetchLikes(
postId, postId,
@ -164,8 +174,19 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme
binding.rvLikes.setLayoutManager(layoutManager); binding.rvLikes.setLayoutManager(layoutManager);
binding.rvLikes.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.HORIZONTAL)); binding.rvLikes.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.HORIZONTAL));
lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { 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; endCursor = null;
}); });
binding.rvLikes.addOnScrollListener(lazyLoader); binding.rvLikes.addOnScrollListener(lazyLoader);

69
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 com.google.common.collect.ImmutableList;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
import java.util.Set; import java.util.Set;
import awais.instagrabber.R; import awais.instagrabber.R;
@ -53,14 +52,15 @@ import awais.instagrabber.db.repositories.FavoriteRepository;
import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.db.repositories.RepositoryCallback;
import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment;
import awais.instagrabber.models.PostsLayoutPreferences; import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.StoryModel;
import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.repositories.requests.StoryViewerOptions; import awais.instagrabber.repositories.requests.StoryViewerOptions;
import awais.instagrabber.repositories.responses.Location; import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
@ -68,6 +68,7 @@ import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.webservices.LocationService; import awais.instagrabber.webservices.LocationService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.StoriesService; import awais.instagrabber.webservices.StoriesService;
import kotlinx.coroutines.Dispatchers;
import static androidx.core.content.PermissionChecker.checkSelfPermission; import static androidx.core.content.PermissionChecker.checkSelfPermission;
import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; 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 (user == null) return;
if (TextUtils.isEmpty(user.getUsername())) { if (TextUtils.isEmpty(user.getUsername())) {
opening = true; opening = true;
graphQLService.fetchPost(feedModel.getCode(), new ServiceCallback<Media>() {
@Override
public void onSuccess(final Media newFeedModel) {
opening = false;
if (newFeedModel == null) return;
openPostDialog(newFeedModel, profilePicView, mainPostImage, position);
}
@Override
public void onFailure(final Throwable t) {
opening = false;
Log.e(TAG, "Error", t);
}
});
graphQLService.fetchPost(
feedModel.getCode(),
CoroutineUtilsKt.getContinuation((media, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
opening = false;
if (throwable != null) {
Log.e(TAG, "Error", throwable);
return;
}
if (media == null) return;
openPostDialog(media, profilePicView, mainPostImage, position);
}))
);
return; return;
} }
opening = true; opening = true;
@ -293,8 +292,8 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
final String cookie = settingsHelper.getString(Constants.COOKIE); final String cookie = settingsHelper.getString(Constants.COOKIE);
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0;
locationService = isLoggedIn ? LocationService.getInstance() : null; 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); setHasOptionsMenu(true);
} }
@ -402,7 +401,16 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
private void fetchLocationModel() { private void fetchLocationModel() {
binding.swipeRefreshLayout.setRefreshing(true); binding.swipeRefreshLayout.setRefreshing(true);
if (isLoggedIn) locationService.fetch(locationId, cb); 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() { private void setupLocationDetails() {
@ -577,22 +585,19 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
storiesFetching = true; storiesFetching = true;
storiesService.getUserStory( storiesService.getUserStory(
StoryViewerOptions.forLocation(locationId, locationModel.getName()), StoryViewerOptions.forLocation(locationId, locationModel.getName()),
new ServiceCallback<List<StoryModel>>() {
@Override
public void onSuccess(final List<StoryModel> storyModels) {
if (storyModels != null && !storyModels.isEmpty()) {
locationDetailsBinding.mainLocationImage.setStoriesBorder(1);
hasStories = true;
}
CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "Error", throwable);
storiesFetching = false; storiesFetching = false;
return;
} }
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error", t);
storiesFetching = false;
if (storyModels != null && !storyModels.isEmpty()) {
locationDetailsBinding.mainLocationImage.setStoriesBorder(1);
hasStories = true;
} }
});
storiesFetching = false;
}), Dispatchers.getIO())
);
} }
} }

60
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.databinding.FragmentNotificationsViewerBinding;
import awais.instagrabber.models.enums.NotificationType; import awais.instagrabber.models.enums.NotificationType;
import awais.instagrabber.repositories.requests.StoryViewerOptions; 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.Notification;
import awais.instagrabber.repositories.responses.notification.NotificationArgs; import awais.instagrabber.repositories.responses.notification.NotificationArgs;
import awais.instagrabber.repositories.responses.notification.NotificationImage; import awais.instagrabber.repositories.responses.notification.NotificationImage;
@ -68,6 +67,7 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
private String type; private String type;
private long targetId; private long targetId;
private Context context; private Context context;
private long userId;
private final ServiceCallback<List<Notification>> cb = new ServiceCallback<List<Notification>>() { private final ServiceCallback<List<Notification>> cb = new ServiceCallback<List<Notification>>() {
@Override @Override
@ -168,34 +168,40 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
break; break;
case 1: case 1:
if (model.getType() == NotificationType.REQUEST) { 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; return;
} }
clickListener.onPreviewClick(model); clickListener.onPreviewClick(model);
break; break;
case 2: 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; break;
} }
}; };
@ -219,11 +225,11 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
if (TextUtils.isEmpty(cookie)) { if (TextUtils.isEmpty(cookie)) {
Toast.makeText(context, R.string.activity_notloggedin, Toast.LENGTH_SHORT).show(); 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); deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID);
csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); 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(); newsService = NewsService.getInstance();
} }

44
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.FeedStoryModel;
import awais.instagrabber.models.HighlightModel; import awais.instagrabber.models.HighlightModel;
import awais.instagrabber.repositories.requests.StoryViewerOptions; import awais.instagrabber.repositories.requests.StoryViewerOptions;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.viewmodels.ArchivesViewModel; import awais.instagrabber.viewmodels.ArchivesViewModel;
import awais.instagrabber.viewmodels.FeedStoriesViewModel; import awais.instagrabber.viewmodels.FeedStoriesViewModel;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.StoriesService; import awais.instagrabber.webservices.StoriesService;
import awais.instagrabber.webservices.StoriesService.ArchiveFetchResponse; import awais.instagrabber.webservices.StoriesService.ArchiveFetchResponse;
import kotlinx.coroutines.Dispatchers;
public final class StoryListViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { public final class StoryListViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = "StoryListViewerFragment"; private static final String TAG = "StoryListViewerFragment";
@ -133,7 +136,7 @@ public final class StoryListViewerFragment extends Fragment implements SwipeRefr
context = getContext(); context = getContext();
if (context == null) return; if (context == null) return;
setHasOptionsMenu(true); setHasOptionsMenu(true);
storiesService = StoriesService.getInstance(null, 0L, null);
storiesService = StoriesService.INSTANCE;
} }
@NonNull @NonNull
@ -239,22 +242,31 @@ public final class StoryListViewerFragment extends Fragment implements SwipeRefr
} }
firstRefresh = false; firstRefresh = false;
} else if (type.equals("feed")) { } 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")) { } 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())
);
} }
} }

253
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;
import awais.instagrabber.repositories.requests.StoryViewerOptions.Type; import awais.instagrabber.repositories.requests.StoryViewerOptions.Type;
import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds; import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds;
import awais.instagrabber.repositories.responses.StoryStickerResponse;
import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
@ -113,6 +112,8 @@ import static awais.instagrabber.utils.Utils.settingsHelper;
public class StoryViewerFragment extends Fragment { public class StoryViewerFragment extends Fragment {
private static final String TAG = "StoryViewerFragment"; private static final String TAG = "StoryViewerFragment";
private final String cookie = settingsHelper.getString(Constants.COOKIE);
private AppCompatActivity fragmentActivity; private AppCompatActivity fragmentActivity;
private View root; private View root;
private FragmentStoryViewerBinding binding; private FragmentStoryViewerBinding binding;
@ -148,21 +149,22 @@ public class StoryViewerFragment extends Fragment {
// private boolean isArchive; // private boolean isArchive;
// private boolean isNotification; // private boolean isNotification;
private DirectMessagesService directMessagesService; private DirectMessagesService directMessagesService;
private final String cookie = settingsHelper.getString(Constants.COOKIE);
private StoryViewerOptions options; private StoryViewerOptions options;
private String csrfToken;
private String deviceId;
private long userId;
@Override @Override
public void onCreate(@Nullable final Bundle savedInstanceState) { public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
if (csrfToken == null) return; 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(); 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); setHasOptionsMenu(true);
} }
@ -220,6 +222,9 @@ public class StoryViewerFragment extends Fragment {
.setTitle(R.string.reply_story) .setTitle(R.string.reply_story)
.setView(input) .setView(input)
.setPositiveButton(R.string.confirm, (d, w) -> directMessagesService.createThread( .setPositiveButton(R.string.confirm, (d, w) -> directMessagesService.createThread(
csrfToken,
userId,
deviceId,
Collections.singletonList(currentStory.getUserId()), Collections.singletonList(currentStory.getUserId()),
null, null,
CoroutineUtilsKt.getContinuation((thread, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { CoroutineUtilsKt.getContinuation((thread, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
@ -229,6 +234,9 @@ public class StoryViewerFragment extends Fragment {
return; return;
} }
directMessagesService.broadcastStoryReply( directMessagesService.broadcastStoryReply(
csrfToken,
userId,
deviceId,
ThreadIdOrUserIds.of(thread.getThreadId()), ThreadIdOrUserIds.of(thread.getThreadId()),
input.getText().toString(), input.getText().toString(),
currentStory.getStoryMediaId(), currentStory.getStoryMediaId(),
@ -514,28 +522,31 @@ public class StoryViewerFragment extends Fragment {
}), (d, w) -> { }), (d, w) -> {
sticking = true; sticking = true;
storiesService.respondToPoll( storiesService.respondToPoll(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId().split("_")[0], currentStory.getStoryMediaId().split("_")[0],
poll.getId(), poll.getId(),
w, w,
new ServiceCallback<StoryStickerResponse>() {
@Override
public void onSuccess(final StoryStickerResponse result) {
sticking = false;
try {
poll.setMyChoice(w);
Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
@Override
public void onFailure(final Throwable t) {
sticking = false;
Log.e(TAG, "Error responding", t);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
});
CoroutineUtilsKt.getContinuation(
(storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
sticking = false;
Log.e(TAG, "Error responding", throwable);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
sticking = false;
try {
poll.setMyChoice(w);
Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}),
Dispatchers.getIO()
)
);
}) })
.setPositiveButton(R.string.cancel, null) .setPositiveButton(R.string.cancel, null)
.show(); .show();
@ -550,27 +561,30 @@ public class StoryViewerFragment extends Fragment {
.setPositiveButton(R.string.confirm, (d, w) -> { .setPositiveButton(R.string.confirm, (d, w) -> {
sticking = true; sticking = true;
storiesService.respondToQuestion( storiesService.respondToQuestion(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId().split("_")[0], currentStory.getStoryMediaId().split("_")[0],
question.getId(), question.getId(),
input.getText().toString(), input.getText().toString(),
new ServiceCallback<StoryStickerResponse>() {
@Override
public void onSuccess(final StoryStickerResponse result) {
sticking = false;
try {
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
@Override
public void onFailure(final Throwable t) {
sticking = false;
Log.e(TAG, "Error responding", t);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
});
CoroutineUtilsKt.getContinuation(
(storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
sticking = false;
Log.e(TAG, "Error responding", throwable);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
sticking = false;
try {
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}),
Dispatchers.getIO()
)
);
}) })
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show(); .show();
@ -605,28 +619,31 @@ public class StoryViewerFragment extends Fragment {
if (quiz.getMyChoice() == -1) { if (quiz.getMyChoice() == -1) {
sticking = true; sticking = true;
storiesService.respondToQuiz( storiesService.respondToQuiz(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId().split("_")[0], currentStory.getStoryMediaId().split("_")[0],
quiz.getId(), quiz.getId(),
w, w,
new ServiceCallback<StoryStickerResponse>() {
@Override
public void onSuccess(final StoryStickerResponse result) {
sticking = false;
try {
quiz.setMyChoice(w);
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
@Override
public void onFailure(final Throwable t) {
sticking = false;
Log.e(TAG, "Error responding", t);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
});
CoroutineUtilsKt.getContinuation(
(storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
sticking = false;
Log.e(TAG, "Error responding", throwable);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
sticking = false;
try {
quiz.setMyChoice(w);
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}),
Dispatchers.getIO()
)
);
} }
}) })
.setPositiveButton(R.string.cancel, null) .setPositiveButton(R.string.cancel, null)
@ -673,28 +690,30 @@ public class StoryViewerFragment extends Fragment {
.setPositiveButton(R.string.confirm, (d, w) -> { .setPositiveButton(R.string.confirm, (d, w) -> {
sticking = true; sticking = true;
storiesService.respondToSlider( storiesService.respondToSlider(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId().split("_")[0], currentStory.getStoryMediaId().split("_")[0],
slider.getId(), slider.getId(),
sliderValue, sliderValue,
new ServiceCallback<StoryStickerResponse>() {
@Override
public void onSuccess(final StoryStickerResponse result) {
sticking = false;
try {
slider.setMyChoice(sliderValue);
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
@Override
public void onFailure(final Throwable t) {
sticking = false;
Log.e(TAG, "Error responding", t);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
});
CoroutineUtilsKt.getContinuation(
(storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
sticking = false;
Log.e(TAG, "Error responding", throwable);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
sticking = false;
try {
slider.setMyChoice(sliderValue);
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}), Dispatchers.getIO()
)
);
}) })
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show(); .show();
@ -786,27 +805,26 @@ public class StoryViewerFragment extends Fragment {
setTitle(type); setTitle(type);
storiesViewModel.getList().setValue(Collections.emptyList()); storiesViewModel.getList().setValue(Collections.emptyList());
if (type == Type.STORY) { if (type == Type.STORY) {
storiesService.fetch(options.getId(), new ServiceCallback<StoryModel>() {
@Override
public void onSuccess(final StoryModel storyModel) {
fetching = false;
binding.storiesList.setVisibility(View.GONE);
if (storyModel == null) {
storiesViewModel.getList().setValue(Collections.emptyList());
currentStory = null;
return;
}
storiesViewModel.getList().setValue(Collections.singletonList(storyModel));
currentStory = storyModel;
refreshStory();
}
@Override
public void onFailure(final Throwable t) {
Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show();
Log.e(TAG, "Error", t);
}
});
storiesService.fetch(
options.getId(),
CoroutineUtilsKt.getContinuation((storyModel, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show();
Log.e(TAG, "Error", throwable);
return;
}
fetching = false;
binding.storiesList.setVisibility(View.GONE);
if (storyModel == null) {
storiesViewModel.getList().setValue(Collections.emptyList());
currentStory = null;
return;
}
storiesViewModel.getList().setValue(Collections.singletonList(storyModel));
currentStory = storyModel;
refreshStory();
}), Dispatchers.getIO())
);
return; return;
} }
if (currentStoryMediaId == null) return; if (currentStoryMediaId == null) return;
@ -840,7 +858,17 @@ public class StoryViewerFragment extends Fragment {
storyCallback.onSuccess(Collections.singletonList(live)); storyCallback.onSuccess(Collections.singletonList(live));
return; 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) { private void setTitle(final Type type) {
@ -944,10 +972,15 @@ public class StoryViewerFragment extends Fragment {
} }
if (settingsHelper.getBoolean(MARK_AS_SEEN)) if (settingsHelper.getBoolean(MARK_AS_SEEN))
storiesService.seen(currentStory.getStoryMediaId(),
currentStory.getTimestamp(),
System.currentTimeMillis() / 1000,
null);
storiesService.seen(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId(),
currentStory.getTimestamp(),
System.currentTimeMillis() / 1000,
CoroutineUtilsKt.getContinuation((s, throwable) -> {}, Dispatchers.getIO())
);
} }
private void downloadStory() { private void downloadStory() {

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

@ -296,8 +296,8 @@ class DirectMessageSettingsFragment : Fragment(), ConfirmDialogFragmentCallback
usersAdapter = DirectUsersAdapter( usersAdapter = DirectUsersAdapter(
inviter?.pk ?: -1, inviter?.pk ?: -1,
{ _: Int, user: User, _: Boolean -> { _: 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 return@DirectUsersAdapter
} }
if (isEmpty(user.username)) 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.TopicCluster;
import awais.instagrabber.repositories.responses.discover.TopicalExploreFeedResponse; import awais.instagrabber.repositories.responses.discover.TopicalExploreFeedResponse;
import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.TopicClusterViewModel; import awais.instagrabber.viewmodels.TopicClusterViewModel;
@ -57,11 +55,11 @@ public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnR
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
fragmentActivity = (MainActivity) requireActivity(); fragmentActivity = (MainActivity) requireActivity();
discoverService = DiscoverService.getInstance(); 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 @Override

40
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.models.PostsLayoutPreferences;
import awais.instagrabber.repositories.requests.StoryViewerOptions; import awais.instagrabber.repositories.requests.StoryViewerOptions;
import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.FeedStoriesViewModel; import awais.instagrabber.viewmodels.FeedStoriesViewModel;
import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.StoriesService; import awais.instagrabber.webservices.StoriesService;
import kotlinx.coroutines.Dispatchers;
import static androidx.core.content.PermissionChecker.checkSelfPermission; import static androidx.core.content.PermissionChecker.checkSelfPermission;
import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; 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) { public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
fragmentActivity = (MainActivity) requireActivity(); fragmentActivity = (MainActivity) requireActivity();
storiesService = StoriesService.getInstance(null, 0L, null);
storiesService = StoriesService.INSTANCE;
setHasOptionsMenu(true); setHasOptionsMenu(true);
} }
@ -428,23 +430,23 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre
// final String cookie = settingsHelper.getString(Constants.COOKIE); // final String cookie = settingsHelper.getString(Constants.COOKIE);
storiesFetching = true; storiesFetching = true;
updateSwipeRefreshState(); updateSwipeRefreshState();
storiesService.getFeedStories(new ServiceCallback<List<FeedStoryModel>>() {
@Override
public void onSuccess(final List<FeedStoryModel> result) {
storiesFetching = false;
feedStoriesViewModel.getList().postValue(result);
feedStoriesAdapter.submitList(result);
if (storyListMenu != null) storyListMenu.setVisible(true);
updateSwipeRefreshState();
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "failed", t);
storiesFetching = false;
updateSwipeRefreshState();
}
});
storiesService.getFeedStories(
CoroutineUtilsKt.getContinuation((feedStoryModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "failed", throwable);
storiesFetching = false;
updateSwipeRefreshState();
return;
}
storiesFetching = false;
//noinspection unchecked
feedStoriesViewModel.getList().postValue((List<FeedStoryModel>) feedStoryModels);
//noinspection unchecked
feedStoriesAdapter.submitList((List<FeedStoryModel>) feedStoryModels);
if (storyListMenu != null) storyListMenu.setVisible(true);
updateSwipeRefreshState();
}), Dispatchers.getIO())
);
} }
private void showPostsLayoutPreferences() { private void showPostsLayoutPreferences() {

332
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.databinding.LayoutProfileDetailsBinding;
import awais.instagrabber.db.datasources.AccountDataSource; import awais.instagrabber.db.datasources.AccountDataSource;
import awais.instagrabber.db.datasources.FavoriteDataSource; import awais.instagrabber.db.datasources.FavoriteDataSource;
import awais.instagrabber.db.entities.Account;
import awais.instagrabber.db.entities.Favorite; import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.db.repositories.AccountRepository; import awais.instagrabber.db.repositories.AccountRepository;
import awais.instagrabber.db.repositories.FavoriteRepository; import awais.instagrabber.db.repositories.FavoriteRepository;
@ -74,12 +73,10 @@ import awais.instagrabber.managers.DirectMessagesManager;
import awais.instagrabber.managers.InboxManager; import awais.instagrabber.managers.InboxManager;
import awais.instagrabber.models.HighlightModel; import awais.instagrabber.models.HighlightModel;
import awais.instagrabber.models.PostsLayoutPreferences; import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.StoryModel;
import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.models.enums.PostItemType; import awais.instagrabber.models.enums.PostItemType;
import awais.instagrabber.repositories.requests.StoryViewerOptions; import awais.instagrabber.repositories.requests.StoryViewerOptions;
import awais.instagrabber.repositories.responses.FriendshipChangeResponse; import awais.instagrabber.repositories.responses.FriendshipChangeResponse;
import awais.instagrabber.repositories.responses.FriendshipRestrictResponse;
import awais.instagrabber.repositories.responses.FriendshipStatus; import awais.instagrabber.repositories.responses.FriendshipStatus;
import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.User;
@ -93,6 +90,7 @@ import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.AppStateViewModel; import awais.instagrabber.viewmodels.AppStateViewModel;
import awais.instagrabber.viewmodels.HighlightsViewModel; import awais.instagrabber.viewmodels.HighlightsViewModel;
import awais.instagrabber.viewmodels.ProfileFragmentViewModel;
import awais.instagrabber.webservices.DirectMessagesService; import awais.instagrabber.webservices.DirectMessagesService;
import awais.instagrabber.webservices.FriendshipService; import awais.instagrabber.webservices.FriendshipService;
import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.GraphQLService;
@ -139,6 +137,14 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
private int downloadChildPosition = -1; private int downloadChildPosition = -1;
private long myId; private long myId;
private PostsLayoutPreferences layoutPreferences = Utils.getPostsLayoutPreferences(Constants.PREF_PROFILE_POSTS_LAYOUT); 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>() { private final ServiceCallback<FriendshipChangeResponse> changeCb = new ServiceCallback<FriendshipChangeResponse>() {
@Override @Override
@ -156,7 +162,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
Log.e(TAG, "Error editing relationship", t); Log.e(TAG, "Error editing relationship", t);
} }
}; };
private final Runnable usernameSettingRunnable = () -> { private final Runnable usernameSettingRunnable = () -> {
final ActionBar actionBar = fragmentActivity.getSupportActionBar(); final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar != null && !TextUtils.isEmpty(username)) { 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 @Override
public void onCreate(@Nullable final Bundle savedInstanceState) { 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); cookie = Utils.settingsHelper.getString(Constants.COOKIE);
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0;
myId = CookieUtils.getUserIdFromCookie(cookie); 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(); 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(); final Context context = getContext();
if (context == null) return; if (context == null) return;
accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(context)); accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(context));
favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(context)); favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(context));
appStateViewModel = new ViewModelProvider(fragmentActivity).get(AppStateViewModel.class); appStateViewModel = new ViewModelProvider(fragmentActivity).get(AppStateViewModel.class);
viewModel = new ViewModelProvider(this).get(ProfileFragmentViewModel.class);
setHasOptionsMenu(true); setHasOptionsMenu(true);
} }
@ -373,6 +374,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
shouldRefresh = false; shouldRefresh = false;
return root; return root;
} }
// appStateViewModel.getCurrentUserLiveData().observe(getViewLifecycleOwner(), user -> viewModel.setCurrentUser(user));
binding = FragmentProfileBinding.inflate(inflater, container, false); binding = FragmentProfileBinding.inflate(inflater, container, false);
root = binding.getRoot(); root = binding.getRoot();
profileDetailsBinding = binding.header; profileDetailsBinding = binding.header;
@ -430,7 +432,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
} }
chainingMenuItem = menu.findItem(R.id.chaining); chainingMenuItem = menu.findItem(R.id.chaining);
if (chainingMenuItem != null) { if (chainingMenuItem != null) {
chainingMenuItem.setVisible(isNotMe && profileModel.hasChaining());
chainingMenuItem.setVisible(isNotMe && profileModel.getHasChaining());
} }
removeFollowerMenuItem = menu.findItem(R.id.remove_follower); removeFollowerMenuItem = menu.findItem(R.id.remove_follower);
if (removeFollowerMenuItem != null) { if (removeFollowerMenuItem != null) {
@ -448,25 +450,38 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
if (!isLoggedIn) return false; if (!isLoggedIn) return false;
final String action = profileModel.getFriendshipStatus().isRestricted() ? "Unrestrict" : "Restrict"; final String action = profileModel.getFriendshipStatus().isRestricted() ? "Unrestrict" : "Restrict";
friendshipService.toggleRestrict( friendshipService.toggleRestrict(
csrfToken,
deviceUuid,
profileModel.getPk(), profileModel.getPk(),
!profileModel.getFriendshipStatus().isRestricted(), !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; return true;
} }
if (item.getItemId() == R.id.block) { if (item.getItemId() == R.id.block) {
if (!isLoggedIn) return false; 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; return true;
} }
if (item.getItemId() == R.id.chaining) { if (item.getItemId() == R.id.chaining) {
@ -481,25 +496,57 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
if (!isLoggedIn) return false; if (!isLoggedIn) return false;
final String action = profileModel.getFriendshipStatus().isMutingReel() ? "Unmute stories" : "Mute stories"; final String action = profileModel.getFriendshipStatus().isMutingReel() ? "Unmute stories" : "Mute stories";
friendshipService.changeMute( friendshipService.changeMute(
csrfToken,
myId,
deviceUuid,
profileModel.getFriendshipStatus().isMutingReel(), profileModel.getFriendshipStatus().isMutingReel(),
profileModel.getPk(), profileModel.getPk(),
true, true,
changeCb);
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
changeCb.onFailure(throwable);
return;
}
changeCb.onSuccess(response);
}), Dispatchers.getIO())
);
return true; return true;
} }
if (item.getItemId() == R.id.mute_posts) { if (item.getItemId() == R.id.mute_posts) {
if (!isLoggedIn) return false; if (!isLoggedIn) return false;
final String action = profileModel.getFriendshipStatus().getMuting() ? "Unmute stories" : "Mute stories"; final String action = profileModel.getFriendshipStatus().getMuting() ? "Unmute stories" : "Mute stories";
friendshipService.changeMute( friendshipService.changeMute(
csrfToken,
myId,
deviceUuid,
profileModel.getFriendshipStatus().getMuting(), profileModel.getFriendshipStatus().getMuting(),
profileModel.getPk(), profileModel.getPk(),
false, false,
changeCb);
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
changeCb.onFailure(throwable);
return;
}
changeCb.onSuccess(response);
}), Dispatchers.getIO())
);
return true; return true;
} }
if (item.getItemId() == R.id.remove_follower) { if (item.getItemId() == R.id.remove_follower) {
if (!isLoggedIn) return false; 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 true;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
@ -583,65 +630,51 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
return; return;
} }
if (isLoggedIn) { 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(); final Context context = getContext();
try {
if (t == null)
Toast.makeText(context, R.string.error_loading_profile_loggedin, Toast.LENGTH_LONG).show();
else
Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show();
} catch (final Throwable ignored) {
}
if (context == null) return;
Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show();
return;
} }
});
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error fetching profile", t);
final Context context = getContext();
try {
if (t == null)
Toast.makeText(context, R.string.error_loading_profile_loggedin, Toast.LENGTH_LONG).show();
else Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show();
} catch (final Throwable ignored) {
}
}
});
userService.getUserFriendship(
user.getPk(),
CoroutineUtilsKt.getContinuation(
(friendshipStatus, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
Log.e(TAG, "Error fetching profile relationship", throwable1);
final Context context = getContext();
if (context == null) return;
Toast.makeText(context, throwable1.getMessage(),
Toast.LENGTH_SHORT).show();
return;
}
user.setFriendshipStatus(friendshipStatus);
profileModel = user;
setProfileDetails();
}), Dispatchers.getIO()
)
);
}), Dispatchers.getIO())
);
return; return;
} }
graphQLService.fetchUser(usernameTemp, new ServiceCallback<User>() {
@Override
public void onSuccess(final User user) {
profileModel = user;
setProfileDetails();
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error fetching profile", t);
final Context context = getContext();
try {
if (t == null)
Toast.makeText(context, R.string.error_loading_profile, Toast.LENGTH_LONG).show();
else Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show();
} catch (final Throwable ignored) {
}
}
});
graphQLService.fetchUser(
usernameTemp,
CoroutineUtilsKt.getContinuation((user, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "Error fetching profile", throwable);
final Context context = getContext();
if (context == null) return;
Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show();
}
profileModel = user;
setProfileDetails();
}))
);
} }
private void setProfileDetails() { private void setProfileDetails() {
@ -856,7 +889,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
profileDetailsBinding.profileContext.setVisibility(View.GONE); profileDetailsBinding.profileContext.setVisibility(View.GONE);
} else { } else {
profileDetailsBinding.profileContext.setVisibility(View.VISIBLE); 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++) { for (int i = 0; i < userProfileContextLinks.size(); i++) {
final UserProfileContextLink link = userProfileContextLinks.get(i); final UserProfileContextLink link = userProfileContextLinks.get(i);
if (link.getUsername() != null) 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); mutePostsMenuItem.setTitle(profileModel.getFriendshipStatus().getMuting() ? R.string.unmute_posts : R.string.mute_posts);
} }
if (chainingMenuItem != null) { if (chainingMenuItem != null) {
chainingMenuItem.setVisible(profileModel.hasChaining());
chainingMenuItem.setVisible(profileModel.getHasChaining());
} }
if (removeFollowerMenuItem != null) { if (removeFollowerMenuItem != null) {
removeFollowerMenuItem.setVisible(profileModel.getFriendshipStatus().getFollowedBy()); removeFollowerMenuItem.setVisible(profileModel.getFriendshipStatus().getFollowedBy());
@ -993,69 +1026,100 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
cookie, cookie,
profileModel.getFullName(), profileModel.getFullName(),
profileModel.getProfilePicUrl(), profileModel.getProfilePicUrl(),
new RepositoryCallback<Account>() {
@Override
public void onSuccess(final Account result) {
accountIsUpdated = true;
CoroutineUtilsKt.getContinuation((account, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "updateAccountInfo: ", throwable);
return;
} }
@Override
public void onDataNotAvailable() {
Log.e(TAG, "onDataNotAvailable: insert failed");
}
});
accountIsUpdated = true;
}), Dispatchers.getIO())
);
} }
private void fetchStoryAndHighlights(final long profileId) { private void fetchStoryAndHighlights(final long profileId) {
storiesService.getUserStory( storiesService.getUserStory(
StoryViewerOptions.forUser(profileId, profileModel.getFullName()), StoryViewerOptions.forUser(profileId, profileModel.getFullName()),
new ServiceCallback<List<StoryModel>>() {
@Override
public void onSuccess(final List<StoryModel> storyModels) {
if (storyModels != null && !storyModels.isEmpty()) {
profileDetailsBinding.mainProfileImage.setStoriesBorder(1);
hasStories = true;
}
CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "Error", throwable);
return;
} }
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error", t);
if (storyModels != null && !storyModels.isEmpty()) {
profileDetailsBinding.mainProfileImage.setStoriesBorder(1);
hasStories = true;
} }
});
storiesService.fetchHighlights(profileId,
new ServiceCallback<List<HighlightModel>>() {
@Override
public void onSuccess(final List<HighlightModel> result) {
if (result != null) {
profileDetailsBinding.highlightsList.setVisibility(View.VISIBLE);
highlightsViewModel.getList().postValue(result);
} else profileDetailsBinding.highlightsList.setVisibility(View.GONE);
}
@Override
public void onFailure(final Throwable t) {
profileDetailsBinding.highlightsList.setVisibility(View.GONE);
Log.e(TAG, "Error", t);
}
});
}), Dispatchers.getIO())
);
storiesService.fetchHighlights(
profileId,
CoroutineUtilsKt.getContinuation((highlightModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
profileDetailsBinding.highlightsList.setVisibility(View.GONE);
Log.e(TAG, "Error", throwable);
return;
}
if (highlightModels != null) {
profileDetailsBinding.highlightsList.setVisibility(View.VISIBLE);
//noinspection unchecked
highlightsViewModel.getList().postValue((List<HighlightModel>) highlightModels);
} else {
profileDetailsBinding.highlightsList.setVisibility(View.GONE);
}
}), Dispatchers.getIO())
);
} }
private void setupCommonListeners() { private void setupCommonListeners() {
final Context context = getContext(); final Context context = getContext();
if (context == null) return;
profileDetailsBinding.btnFollow.setOnClickListener(v -> { profileDetailsBinding.btnFollow.setOnClickListener(v -> {
if (profileModel.getFriendshipStatus().getFollowing() && profileModel.isPrivate()) { if (profileModel.getFriendshipStatus().getFollowing() && profileModel.isPrivate()) {
new AlertDialog.Builder(context) new AlertDialog.Builder(context)
.setTitle(R.string.priv_acc) .setTitle(R.string.priv_acc)
.setMessage(R.string.priv_acc_confirm) .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) .setNegativeButton(R.string.cancel, null)
.show(); .show();
} else if (profileModel.getFriendshipStatus().getFollowing() || profileModel.getFriendshipStatus().getOutgoingRequest()) { } 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 { } 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 -> { profileDetailsBinding.btnSaved.setOnClickListener(v -> {
@ -1078,6 +1142,9 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
profileDetailsBinding.btnDM.setOnClickListener(v -> { profileDetailsBinding.btnDM.setOnClickListener(v -> {
profileDetailsBinding.btnDM.setEnabled(false); profileDetailsBinding.btnDM.setEnabled(false);
directMessagesService.createThread( directMessagesService.createThread(
csrfToken,
myId,
deviceUuid,
Collections.singletonList(profileModel.getPk()), Collections.singletonList(profileModel.getPk()),
null, null,
CoroutineUtilsKt.getContinuation((thread, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { CoroutineUtilsKt.getContinuation((thread, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
@ -1120,7 +1187,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
} }
showProfilePicDialog(); showProfilePicDialog();
}; };
if (context == null) return;
new AlertDialog.Builder(context) new AlertDialog.Builder(context)
.setItems(options, profileDialogListener) .setItems(options, profileDialogListener)
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)

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

@ -11,7 +11,6 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
@ -24,28 +23,24 @@ import androidx.preference.PreferenceScreen;
import androidx.preference.PreferenceViewHolder; import androidx.preference.PreferenceViewHolder;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import awais.instagrabber.BuildConfig; import awais.instagrabber.BuildConfig;
import awais.instagrabber.R; import awais.instagrabber.R;
import awais.instagrabber.activities.Login; import awais.instagrabber.activities.Login;
import awais.instagrabber.activities.MainActivity; import awais.instagrabber.activities.MainActivity;
import awais.instagrabber.databinding.PrefAccountSwitcherBinding; import awais.instagrabber.databinding.PrefAccountSwitcherBinding;
import awais.instagrabber.db.datasources.AccountDataSource; import awais.instagrabber.db.datasources.AccountDataSource;
import awais.instagrabber.db.entities.Account;
import awais.instagrabber.db.repositories.AccountRepository; import awais.instagrabber.db.repositories.AccountRepository;
import awais.instagrabber.db.repositories.RepositoryCallback;
import awais.instagrabber.dialogs.AccountSwitcherDialogFragment; import awais.instagrabber.dialogs.AccountSwitcherDialogFragment;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.FlavorTown; import awais.instagrabber.utils.FlavorTown;
import awais.instagrabber.utils.ProcessPhoenix; import awais.instagrabber.utils.ProcessPhoenix;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.UserService; import awais.instagrabber.webservices.UserService;
import kotlinx.coroutines.Dispatchers;
import static awais.instagrabber.utils.Utils.settingsHelper; import static awais.instagrabber.utils.Utils.settingsHelper;
@ -98,75 +93,77 @@ public class MorePreferencesFragment extends BasePreferencesFragment {
return true; return true;
})); }));
} }
accountRepository.getAllAccounts(new RepositoryCallback<List<Account>>() {
@Override
public void onSuccess(@NonNull final List<Account> accounts) {
if (!isLoggedIn) {
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();
final AccountSwitcherPreference preference = getAccountSwitcherPreference(null, context1);
if (preference == null) return;
accountCategory.addPreference(preference);
}
// Need to show something to trigger login activity
final Preference preference1 = getPreference(R.string.add_account, R.drawable.ic_add, preference -> {
final Context context1 = getContext();
if (context1 == null) return false;
startActivityForResult(new Intent(context1, Login.class), Constants.LOGIN_RESULT_CODE);
return true;
});
if (preference1 == null) return;
accountCategory.addPreference(preference1);
}
if (accounts.size() > 0) { if (accounts.size() > 0) {
final Context context1 = getContext();
final AccountSwitcherPreference preference = getAccountSwitcherPreference(null, context1);
if (preference == null) return;
accountCategory.addPreference(preference);
final Preference preference1 = getPreference(
R.string.remove_all_acc,
null,
R.drawable.ic_account_multiple_remove_24,
preference -> {
if (getContext() == null) return false;
new AlertDialog.Builder(getContext())
.setTitle(R.string.logout)
.setMessage(R.string.remove_all_acc_warning)
.setPositiveButton(R.string.yes, (dialog, which) -> {
final Context context1 = getContext();
if (context1 == null) return;
CookieUtils.removeAllAccounts(
context1,
CoroutineUtilsKt.getContinuation(
(unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
return;
}
final Context context2 = getContext();
if (context2 == null) return;
Toast.makeText(context2, R.string.logout_success, Toast.LENGTH_SHORT).show();
settingsHelper.putString(Constants.COOKIE, "");
AppExecutors.INSTANCE
.getMainThread()
.execute(() -> ProcessPhoenix.triggerRebirth(context1), 200);
}),
Dispatchers.getIO()
)
);
})
.setNegativeButton(R.string.cancel, null)
.show();
return true;
});
if (preference1 == null) return;
accountCategory.addPreference(preference1);
} }
// Need to show something to trigger login activity
final Preference preference1 = getPreference(R.string.add_account, R.drawable.ic_add, preference -> {
final Context context1 = getContext();
if (context1 == null) return false;
startActivityForResult(new Intent(context1, Login.class), Constants.LOGIN_RESULT_CODE);
return true;
});
if (preference1 == null) return;
accountCategory.addPreference(preference1);
}
if (accounts.size() > 0) {
final Preference preference1 = getPreference(
R.string.remove_all_acc,
null,
R.drawable.ic_account_multiple_remove_24,
preference -> {
if (getContext() == null) return false;
new AlertDialog.Builder(getContext())
.setTitle(R.string.logout)
.setMessage(R.string.remove_all_acc_warning)
.setPositiveButton(R.string.yes, (dialog, which) -> {
final Context context1 = getContext();
if (context1 == null) return;
CookieUtils.removeAllAccounts(context1, new RepositoryCallback<Void>() {
@Override
public void onSuccess(final Void result) {
// shouldRecreate();
final Context context1 = getContext();
if (context1 == null) return;
Toast.makeText(context1, R.string.logout_success, Toast.LENGTH_SHORT).show();
settingsHelper.putString(Constants.COOKIE, "");
AppExecutors.INSTANCE.getMainThread().execute(() -> ProcessPhoenix.triggerRebirth(context1), 200);
}
@Override
public void onDataNotAvailable() {}
});
})
.setNegativeButton(R.string.cancel, null)
.show();
return true;
});
if (preference1 == null) return;
accountCategory.addPreference(preference1);
}
}
@Override
public void onDataNotAvailable() {
Log.d(TAG, "onDataNotAvailable");
if (!isLoggedIn) {
// Need to show something to trigger login activity
accountCategory.addPreference(getPreference(R.string.add_account, R.drawable.ic_add, preference -> {
startActivityForResult(new Intent(getContext(), Login.class), Constants.LOGIN_RESULT_CODE);
return true;
}));
}
}
});
}), Dispatchers.getIO())
);
// final PreferenceCategory generalCategory = new PreferenceCategory(context); // final PreferenceCategory generalCategory = new PreferenceCategory(context);
// generalCategory.setTitle(R.string.pref_category_general); // generalCategory.setTitle(R.string.pref_category_general);
@ -288,44 +285,33 @@ public class MorePreferencesFragment extends BasePreferencesFragment {
// adds cookies to database for quick access // adds cookies to database for quick access
final long uid = CookieUtils.getUserIdFromCookie(cookie); final long uid = CookieUtils.getUserIdFromCookie(cookie);
final UserService userService = UserService.getInstance();
userService.getUserInfo(uid, new ServiceCallback<User>() {
@Override
public void onSuccess(final User result) {
// Log.d(TAG, "adding userInfo: " + result);
if (result != null) {
accountRepository.insertOrUpdateAccount(
uid,
result.getUsername(),
cookie,
result.getFullName(),
result.getProfilePicUrl(),
new RepositoryCallback<Account>() {
@Override
public void onSuccess(final Account result) {
// final FragmentActivity activity = getActivity();
// if (activity == null) return;
// activity.recreate();
AppExecutors.INSTANCE.getMainThread().execute(() -> {
final Context context = getContext();
if (context == null) return;
ProcessPhoenix.triggerRebirth(context);
}, 200);
}
@Override
public void onDataNotAvailable() {
Log.e(TAG, "onDataNotAvailable: insert failed");
}
});
}
final UserService userService = UserService.INSTANCE;
userService.getUserInfo(uid, CoroutineUtilsKt.getContinuation((user, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "Error fetching user info", throwable);
return;
} }
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error fetching user info", t);
if (user != null) {
accountRepository.insertOrUpdateAccount(
uid,
user.getUsername(),
cookie,
user.getFullName(),
user.getProfilePicUrl(),
CoroutineUtilsKt.getContinuation((account, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
Log.e(TAG, "onActivityResult: ", throwable1);
return;
}
AppExecutors.INSTANCE.getMainThread().execute(() -> {
final Context context = getContext();
if (context == null) return;
ProcessPhoenix.triggerRebirth(context);
}, 200);
}), Dispatchers.getIO())
);
} }
});
}), Dispatchers.getIO()));
} }
} }
@ -419,20 +405,21 @@ public class MorePreferencesFragment extends BasePreferencesFragment {
final PrefAccountSwitcherBinding binding = PrefAccountSwitcherBinding.bind(root); final PrefAccountSwitcherBinding binding = PrefAccountSwitcherBinding.bind(root);
final long uid = CookieUtils.getUserIdFromCookie(cookie); final long uid = CookieUtils.getUserIdFromCookie(cookie);
if (uid <= 0) return; if (uid <= 0) return;
accountRepository.getAccount(uid, new RepositoryCallback<Account>() {
@Override
public void onSuccess(final Account account) {
binding.getRoot().post(() -> {
binding.fullName.setText(account.getFullName());
binding.username.setText("@" + account.getUsername());
binding.profilePic.setImageURI(account.getProfilePic());
binding.getRoot().requestLayout();
});
}
@Override
public void onDataNotAvailable() {}
});
accountRepository.getAccount(
uid,
CoroutineUtilsKt.getContinuation((account, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "onBindViewHolder: ", throwable);
return;
}
binding.getRoot().post(() -> {
binding.fullName.setText(account.getFullName());
binding.username.setText("@" + account.getUsername());
binding.profilePic.setImageURI(account.getProfilePic());
binding.getRoot().requestLayout();
});
}), Dispatchers.getIO())
);
} }
} }
} }

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

@ -4,7 +4,6 @@ import android.content.ContentResolver
import android.util.Log import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import awais.instagrabber.managers.ThreadManager.Companion.getInstance
import awais.instagrabber.models.Resource import awais.instagrabber.models.Resource
import awais.instagrabber.models.Resource.Companion.error import awais.instagrabber.models.Resource.Companion.error
import awais.instagrabber.models.Resource.Companion.loading 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.getCsrfTokenFromCookie
import awais.instagrabber.utils.getUserIdFromCookie import awais.instagrabber.utils.getUserIdFromCookie
import awais.instagrabber.webservices.DirectMessagesService import awais.instagrabber.webservices.DirectMessagesService
import awais.instagrabber.webservices.DirectMessagesService.Companion.getInstance
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.*
object DirectMessagesManager { 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 TAG = DirectMessagesManager::class.java.simpleName
private val viewerId: Long private val viewerId: Long
private val deviceUuid: String private val deviceUuid: String
private val csrfToken: String private val csrfToken: String
private val service: DirectMessagesService
fun moveThreadFromPending(threadId: String) { fun moveThreadFromPending(threadId: String) {
val pendingThreads = pendingInboxManager.threads.value ?: return val pendingThreads = pendingInboxManager.threads.value ?: return
@ -65,10 +62,10 @@ object DirectMessagesManager {
currentUser: User, currentUser: User,
contentResolver: ContentResolver, contentResolver: ContentResolver,
): ThreadManager { ): 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) { fun sendMedia(recipients: Set<RankedRecipient>, mediaId: String, scope: CoroutineScope) {
val resultsCount = intArrayOf(0) val resultsCount = intArrayOf(0)
@ -134,7 +131,10 @@ object DirectMessagesManager {
data.postValue(loading(null)) data.postValue(loading(null))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
service.broadcastMediaShare(
DirectMessagesService.broadcastMediaShare(
csrfToken,
viewerId,
deviceUuid,
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
of(threadId), of(threadId),
mediaId mediaId
@ -157,6 +157,5 @@ object DirectMessagesManager {
val csrfToken = getCsrfTokenFromCookie(cookie) val csrfToken = getCsrfTokenFromCookie(cookie)
require(!csrfToken.isNullOrBlank() && viewerId != 0L && deviceUuid.isNotBlank()) { "User is not logged in!" } require(!csrfToken.isNullOrBlank() && viewerId != 0L && deviceUuid.isNotBlank()) { "User is not logged in!" }
this.csrfToken = csrfToken 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.User
import awais.instagrabber.repositories.responses.directmessages.* import awais.instagrabber.repositories.responses.directmessages.*
import awais.instagrabber.utils.* import awais.instagrabber.utils.*
import awais.instagrabber.utils.extensions.TAG
import awais.instagrabber.webservices.DirectMessagesService import awais.instagrabber.webservices.DirectMessagesService
import awais.instagrabber.webservices.DirectMessagesService.Companion.getInstance
import com.google.common.cache.CacheBuilder import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader import com.google.common.cache.CacheLoader
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
@ -24,14 +24,13 @@ import retrofit2.Call
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit 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 fetchInboxControlledRunner: ControlledRunner<Resource<DirectInbox>> = ControlledRunner()
// private val fetchPendingInboxControlledRunner: ControlledRunner<Resource<DirectInbox>> = ControlledRunner() // private val fetchPendingInboxControlledRunner: ControlledRunner<Resource<DirectInbox>> = ControlledRunner()
private val inbox = MutableLiveData<Resource<DirectInbox?>>(success(null)) private val inbox = MutableLiveData<Resource<DirectInbox?>>(success(null))
private val unseenCount = MutableLiveData<Resource<Int?>>() private val unseenCount = MutableLiveData<Resource<Int?>>()
private val pendingRequestsTotal = MutableLiveData(0) private val pendingRequestsTotal = MutableLiveData(0)
val threads: LiveData<List<DirectThread>> val threads: LiveData<List<DirectThread>>
private val service: DirectMessagesService
private var inboxRequest: Call<DirectInboxResponse?>? = null private var inboxRequest: Call<DirectInboxResponse?>? = null
private var unseenCountRequest: Call<DirectBadgeCount?>? = null private var unseenCountRequest: Call<DirectBadgeCount?>? = null
private var seqId: Long = 0 private var seqId: Long = 0
@ -58,7 +57,11 @@ class InboxManager private constructor(private val pending: Boolean) {
inbox.postValue(loading(currentDirectInbox)) inbox.postValue(loading(currentDirectInbox))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { 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) parseInboxResponse(inboxValue)
} catch (e: Exception) { } catch (e: Exception) {
inbox.postValue(error(e.message, currentDirectInbox)) inbox.postValue(error(e.message, currentDirectInbox))
@ -74,7 +77,7 @@ class InboxManager private constructor(private val pending: Boolean) {
unseenCount.postValue(loading(currentUnseenCount)) unseenCount.postValue(loading(currentUnseenCount))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val directBadgeCount = service.fetchUnseenCount()
val directBadgeCount = DirectMessagesService.fetchUnseenCount()
unseenCount.postValue(success(directBadgeCount.badgeCount)) unseenCount.postValue(success(directBadgeCount.badgeCount))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed fetching unseen count", e) Log.e(TAG, "Failed fetching unseen count", e)
@ -286,7 +289,6 @@ class InboxManager private constructor(private val pending: Boolean) {
} }
companion object { companion object {
private val TAG = InboxManager::class.java.simpleName
private val THREAD_LOCKS = CacheBuilder private val THREAD_LOCKS = CacheBuilder
.newBuilder() .newBuilder()
.expireAfterAccess(1, TimeUnit.MINUTES) // max lock time ever expected .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 if (t2FirstDirectItem == null) return@Comparator -1
t2FirstDirectItem.getTimestamp().compareTo(t1FirstDirectItem.getTimestamp()) t2FirstDirectItem.getTimestamp().compareTo(t1FirstDirectItem.getTimestamp())
} }
fun getInstance(pending: Boolean): InboxManager {
return InboxManager(pending)
}
} }
init { init {
@ -311,7 +309,6 @@ class InboxManager private constructor(private val pending: Boolean) {
val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID) val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
val csrfToken = getCsrfTokenFromCookie(cookie) val csrfToken = getCsrfTokenFromCookie(cookie)
require(!csrfToken.isNullOrBlank() && viewerId != 0L && deviceUuid.isNotBlank()) { "User is not logged in!" } require(!csrfToken.isNullOrBlank() && viewerId != 0L && deviceUuid.isNotBlank()) { "User is not logged in!" }
service = getInstance(csrfToken, viewerId, deviceUuid)
// Transformations // Transformations
threads = Transformations.distinctUntilChanged(Transformations.map(inbox) { inboxResource: Resource<DirectInbox?> -> threads = Transformations.distinctUntilChanged(Transformations.map(inbox) { inboxResource: Resource<DirectInbox?> ->

217
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.VideoOptions
import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds
import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds.Companion.of 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.User
import awais.instagrabber.repositories.responses.directmessages.* import awais.instagrabber.repositories.responses.directmessages.*
import awais.instagrabber.repositories.responses.giphy.GiphyGif 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.OnInfoLoadListener
import awais.instagrabber.utils.MediaUtils.VideoInfo import awais.instagrabber.utils.MediaUtils.VideoInfo
import awais.instagrabber.utils.TextUtils.isEmpty import awais.instagrabber.utils.TextUtils.isEmpty
import awais.instagrabber.utils.extensions.TAG
import awais.instagrabber.webservices.DirectMessagesService import awais.instagrabber.webservices.DirectMessagesService
import awais.instagrabber.webservices.FriendshipService import awais.instagrabber.webservices.FriendshipService
import awais.instagrabber.webservices.MediaService import awais.instagrabber.webservices.MediaService
import awais.instagrabber.webservices.ServiceCallback
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.collect.Iterables import com.google.common.collect.Iterables
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -45,17 +43,16 @@ import java.io.File
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.stream.Collectors import java.util.stream.Collectors
class ThreadManager private constructor(
class ThreadManager(
private val threadId: String, private val threadId: String,
pending: Boolean, 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?>>() private val _fetching = MutableLiveData<Resource<Any?>>()
val fetching: LiveData<Resource<Any?>> = _fetching val fetching: LiveData<Resource<Any?>> = _fetching
@ -64,13 +61,7 @@ class ThreadManager private constructor(
private val _pendingRequests = MutableLiveData<DirectThreadParticipantRequestsResponse?>(null) private val _pendingRequests = MutableLiveData<DirectThreadParticipantRequestsResponse?>(null)
val pendingRequests: LiveData<DirectThreadParticipantRequestsResponse?> = _pendingRequests val pendingRequests: LiveData<DirectThreadParticipantRequestsResponse?> = _pendingRequests
private val inboxManager: InboxManager = if (pending) DirectMessagesManager.pendingInboxManager else DirectMessagesManager.inboxManager private val inboxManager: InboxManager = if (pending) DirectMessagesManager.pendingInboxManager else DirectMessagesManager.inboxManager
private val viewerId: Long
private val threadIdOrUserIds: ThreadIdOrUserIds = of(threadId) 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 { val thread: LiveData<DirectThread?> by lazy {
distinctUntilChanged(map(inboxManager.getInbox()) { inboxResource: Resource<DirectInbox?>? -> distinctUntilChanged(map(inboxManager.getInbox()) { inboxResource: Resource<DirectInbox?>? ->
@ -135,7 +126,7 @@ class ThreadManager private constructor(
_fetching.postValue(loading(null)) _fetching.postValue(loading(null))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val threadFeedResponse = service.fetchThread(threadId, cursor)
val threadFeedResponse = DirectMessagesService.fetchThread(threadId, cursor)
if (threadFeedResponse.status != null && threadFeedResponse.status != "ok") { if (threadFeedResponse.status != null && threadFeedResponse.status != "ok") {
_fetching.postValue(error(R.string.generic_not_ok_response, null)) _fetching.postValue(error(R.string.generic_not_ok_response, null))
return@launch return@launch
@ -163,7 +154,7 @@ class ThreadManager private constructor(
if (isGroup == null || !isGroup) return if (isGroup == null || !isGroup) return
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = service.participantRequests(threadId, 1)
val response = DirectMessagesService.participantRequests(threadId, 1)
_pendingRequests.postValue(response) _pendingRequests.postValue(response)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "fetchPendingRequests: ", e) Log.e(TAG, "fetchPendingRequests: ", e)
@ -355,7 +346,10 @@ class ThreadManager private constructor(
val repliedToClientContext = replyToItemValue?.clientContext val repliedToClientContext = replyToItemValue?.clientContext
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = service.broadcastText(
val response = DirectMessagesService.broadcastText(
csrfToken,
viewerId,
deviceUuid,
clientContext, clientContext,
threadIdOrUserIds, threadIdOrUserIds,
text, text,
@ -410,7 +404,10 @@ class ThreadManager private constructor(
data.postValue(loading(directItem)) data.postValue(loading(directItem))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val request = service.broadcastAnimatedMedia(
val request = DirectMessagesService.broadcastAnimatedMedia(
csrfToken,
userId,
deviceUuid,
clientContext, clientContext,
threadIdOrUserIds, threadIdOrUserIds,
giphyGif giphyGif
@ -455,8 +452,11 @@ class ThreadManager private constructor(
"4", "4",
null null
) )
mediaService.uploadFinish(uploadFinishOptions)
val broadcastResponse = service.broadcastVoice(
MediaService.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions)
val broadcastResponse = DirectMessagesService.broadcastVoice(
csrfToken,
viewerId,
deviceUuid,
clientContext, clientContext,
threadIdOrUserIds, threadIdOrUserIds,
uploadDmVoiceOptions.uploadId, uploadDmVoiceOptions.uploadId,
@ -497,7 +497,10 @@ class ThreadManager private constructor(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
service.broadcastReaction(
DirectMessagesService.broadcastReaction(
csrfToken,
userId,
deviceUuid,
clientContext, clientContext,
threadIdOrUserIds, threadIdOrUserIds,
itemId, itemId,
@ -534,7 +537,16 @@ class ThreadManager private constructor(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
service.broadcastReaction(clientContext, threadIdOrUserIds, itemId1, null, true)
DirectMessagesService.broadcastReaction(
csrfToken,
viewerId,
deviceUuid,
clientContext,
threadIdOrUserIds,
itemId1,
null,
true
)
} catch (e: Exception) { } catch (e: Exception) {
data.postValue(error(e.message, null)) data.postValue(error(e.message, null))
Log.e(TAG, "sendDeleteReaction: ", e) Log.e(TAG, "sendDeleteReaction: ", e)
@ -553,7 +565,7 @@ class ThreadManager private constructor(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
service.deleteItem(threadId, itemId)
DirectMessagesService.deleteItem(csrfToken, deviceUuid, threadId, itemId)
} catch (e: Exception) { } catch (e: Exception) {
// add the item back if unsuccessful // add the item back if unsuccessful
addItems(index, listOf(item)) addItems(index, listOf(item))
@ -629,7 +641,7 @@ class ThreadManager private constructor(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
service.forward(
DirectMessagesService.forward(
thread.threadId, thread.threadId,
itemTypeName, itemTypeName,
threadId, threadId,
@ -648,7 +660,7 @@ class ThreadManager private constructor(
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
service.approveRequest(threadId)
DirectMessagesService.approveRequest(csrfToken, deviceUuid, threadId)
data.postValue(success(Any())) data.postValue(success(Any()))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "acceptRequest: ", e) Log.e(TAG, "acceptRequest: ", e)
@ -662,7 +674,7 @@ class ThreadManager private constructor(
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
service.declineRequest(threadId)
DirectMessagesService.declineRequest(csrfToken, deviceUuid, threadId)
data.postValue(success(Any())) data.postValue(success(Any()))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "declineRequest: ", e) Log.e(TAG, "declineRequest: ", e)
@ -707,9 +719,8 @@ class ThreadManager private constructor(
height: Int, height: Int,
scope: CoroutineScope, scope: CoroutineScope,
) { ) {
val userId = getCurrentUserId(data) ?: return
val clientContext = UUID.randomUUID().toString() 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 directItem.isPending = true
addItems(0, listOf(directItem)) addItems(0, listOf(directItem))
data.postValue(loading(directItem)) data.postValue(loading(directItem))
@ -719,7 +730,7 @@ class ThreadManager private constructor(
if (handleInvalidResponse(data, response)) return@launch if (handleInvalidResponse(data, response)) return@launch
val response1 = response.response ?: return@launch val response1 = response.response ?: return@launch
val uploadId = response1.optString("upload_id") 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) parseResponse(response2, data, directItem)
} catch (e: Exception) { } catch (e: Exception) {
data.postValue(error(e.message, null)) data.postValue(error(e.message, null))
@ -779,8 +790,11 @@ class ThreadManager private constructor(
"2", "2",
VideoOptions(duration / 1000f, emptyList(), 0, false) 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, clientContext,
threadIdOrUserIds, threadIdOrUserIds,
uploadDmVideoOptions.uploadId, uploadDmVideoOptions.uploadId,
@ -907,7 +921,7 @@ class ThreadManager private constructor(
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = service.updateTitle(threadId, newTitle.trim())
val response = DirectMessagesService.updateTitle(csrfToken, deviceUuid, threadId, newTitle.trim())
handleDetailsChangeResponse(data, response) handleDetailsChangeResponse(data, response)
} catch (e: Exception) { } catch (e: Exception) {
} }
@ -919,7 +933,9 @@ class ThreadManager private constructor(
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = service.addUsers(
val response = DirectMessagesService.addUsers(
csrfToken,
deviceUuid,
threadId, threadId,
users.map { obj: User -> obj.pk } users.map { obj: User -> obj.pk }
) )
@ -936,7 +952,7 @@ class ThreadManager private constructor(
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
service.removeUsers(threadId, setOf(user.pk))
DirectMessagesService.removeUsers(csrfToken, deviceUuid, threadId, setOf(user.pk))
data.postValue(success(Any())) data.postValue(success(Any()))
var activeUsers = users.value var activeUsers = users.value
var leftUsersValue = leftUsers.value var leftUsersValue = leftUsers.value
@ -971,7 +987,7 @@ class ThreadManager private constructor(
if (isAdmin(user)) return data if (isAdmin(user)) return data
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
service.addAdmins(threadId, setOf(user.pk))
DirectMessagesService.addAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk))
val currentAdminIds = adminUserIds.value val currentAdminIds = adminUserIds.value
val updatedAdminIds = ImmutableList.builder<Long>() val updatedAdminIds = ImmutableList.builder<Long>()
.addAll(currentAdminIds ?: emptyList()) .addAll(currentAdminIds ?: emptyList())
@ -999,7 +1015,7 @@ class ThreadManager private constructor(
if (!isAdmin(user)) return data if (!isAdmin(user)) return data
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
service.removeAdmins(threadId, setOf(user.pk))
DirectMessagesService.removeAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk))
val currentAdmins = adminUserIds.value ?: return@launch val currentAdmins = adminUserIds.value ?: return@launch
val updatedAdminUserIds = currentAdmins.filter { userId1: Long -> userId1 != user.pk } val updatedAdminUserIds = currentAdmins.filter { userId1: Long -> userId1 != user.pk }
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
@ -1029,7 +1045,7 @@ class ThreadManager private constructor(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
service.mute(threadId)
DirectMessagesService.mute(csrfToken, deviceUuid, threadId)
data.postValue(success(Any())) data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
try { try {
@ -1057,7 +1073,7 @@ class ThreadManager private constructor(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
service.unmute(threadId)
DirectMessagesService.unmute(csrfToken, deviceUuid, threadId)
data.postValue(success(Any())) data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
try { try {
@ -1085,7 +1101,7 @@ class ThreadManager private constructor(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
service.muteMentions(threadId)
DirectMessagesService.muteMentions(csrfToken, deviceUuid, threadId)
data.postValue(success(Any())) data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
try { try {
@ -1113,7 +1129,7 @@ class ThreadManager private constructor(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
service.unmuteMentions(threadId)
DirectMessagesService.unmuteMentions(csrfToken, deviceUuid, threadId)
data.postValue(success(Any())) data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
try { try {
@ -1133,61 +1149,57 @@ class ThreadManager private constructor(
fun blockUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> { fun blockUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<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) 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 return data
} }
fun unblockUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> { fun unblockUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<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) 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 return data
} }
fun restrictUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> { fun restrictUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<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) 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 return data
} }
fun unRestrictUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> { fun unRestrictUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<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) 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 return data
} }
@ -1196,7 +1208,9 @@ class ThreadManager private constructor(
data.postValue(loading(null)) data.postValue(loading(null))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = service.approveParticipantRequests(
val response = DirectMessagesService.approveParticipantRequests(
csrfToken,
deviceUuid,
threadId, threadId,
users.map { obj: User -> obj.pk } users.map { obj: User -> obj.pk }
) )
@ -1215,7 +1229,9 @@ class ThreadManager private constructor(
data.postValue(loading(null)) data.postValue(loading(null))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = service.declineParticipantRequests(
val response = DirectMessagesService.declineParticipantRequests(
csrfToken,
deviceUuid,
threadId, threadId,
users.map { obj: User -> obj.pk } users.map { obj: User -> obj.pk }
) )
@ -1255,7 +1271,7 @@ class ThreadManager private constructor(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = service.approvalRequired(threadId)
val response = DirectMessagesService.approvalRequired(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, response) handleDetailsChangeResponse(data, response)
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
try { try {
@ -1283,7 +1299,7 @@ class ThreadManager private constructor(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val request = service.approvalNotRequired(threadId)
val request = DirectMessagesService.approvalNotRequired(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, request) handleDetailsChangeResponse(data, request)
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
try { try {
@ -1306,7 +1322,7 @@ class ThreadManager private constructor(
data.postValue(loading(null)) data.postValue(loading(null))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val request = service.leave(threadId)
val request = DirectMessagesService.leave(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, request) handleDetailsChangeResponse(data, request)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "leave: ", e) Log.e(TAG, "leave: ", e)
@ -1321,7 +1337,7 @@ class ThreadManager private constructor(
data.postValue(loading(null)) data.postValue(loading(null))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val request = service.end(threadId)
val request = DirectMessagesService.end(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, request) handleDetailsChangeResponse(data, request)
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
try { try {
@ -1358,7 +1374,7 @@ class ThreadManager private constructor(
data.postValue(loading(null)) data.postValue(loading(null))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = service.markAsSeen(threadId, directItem)
val response = DirectMessagesService.markAsSeen(csrfToken, deviceUuid, threadId, directItem)
if (response == null) { if (response == null) {
data.postValue(error(R.string.generic_null_response, null)) data.postValue(error(R.string.generic_null_response, null))
return@launch return@launch
@ -1381,43 +1397,4 @@ class ThreadManager private constructor(
} }
return data 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 android.webkit.CookieManager
import awais.instagrabber.db.datasources.AccountDataSource import awais.instagrabber.db.datasources.AccountDataSource
import awais.instagrabber.db.repositories.AccountRepository import awais.instagrabber.db.repositories.AccountRepository
import awais.instagrabber.db.repositories.RepositoryCallback
import java.net.CookiePolicy import java.net.CookiePolicy
import java.net.HttpCookie import java.net.HttpCookie
import java.net.URI 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() 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 { fun getUserIdFromCookie(cookies: String?): Long {

50
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.interfaces.FetchListener;
import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.utils.PasswordUtils.IncorrectPasswordException; import awais.instagrabber.utils.PasswordUtils.IncorrectPasswordException;
import kotlinx.coroutines.Dispatchers;
import static awais.instagrabber.utils.Utils.settingsHelper; import static awais.instagrabber.utils.Utils.settingsHelper;
@ -396,33 +397,32 @@ public final class ExportImportUtils {
private static ListenableFuture<JSONArray> getCookies(final Context context) { private static ListenableFuture<JSONArray> getCookies(final Context context) {
final SettableFuture<JSONArray> future = SettableFuture.create(); final SettableFuture<JSONArray> future = SettableFuture.create();
final AccountRepository accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(context)); final AccountRepository accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(context));
accountRepository.getAllAccounts(new RepositoryCallback<List<Account>>() {
@Override
public void onSuccess(final List<Account> accounts) {
final JSONArray jsonArray = new JSONArray();
try {
for (final Account cookie : accounts) {
final JSONObject jsonObject = new JSONObject();
jsonObject.put("i", cookie.getUid());
jsonObject.put("u", cookie.getUsername());
jsonObject.put("c", cookie.getCookie());
jsonObject.put("full_name", cookie.getFullName());
jsonObject.put("profile_pic", cookie.getProfilePic());
jsonArray.put(jsonObject);
accountRepository.getAllAccounts(
CoroutineUtilsKt.getContinuation((accounts, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "getCookies: ", throwable);
future.set(new JSONArray());
return;
} }
} catch (Exception e) {
if (BuildConfig.DEBUG) {
Log.e(TAG, "Error exporting accounts", e);
final JSONArray jsonArray = new JSONArray();
try {
for (final Account cookie : accounts) {
final JSONObject jsonObject = new JSONObject();
jsonObject.put("i", cookie.getUid());
jsonObject.put("u", cookie.getUsername());
jsonObject.put("c", cookie.getCookie());
jsonObject.put("full_name", cookie.getFullName());
jsonObject.put("profile_pic", cookie.getProfilePic());
jsonArray.put(jsonObject);
}
} catch (Exception e) {
if (BuildConfig.DEBUG) {
Log.e(TAG, "Error exporting accounts", e);
}
} }
}
future.set(jsonArray);
}
@Override
public void onDataNotAvailable() {
future.set(new JSONArray());
}
});
future.set(jsonArray);
}), Dispatchers.getIO())
);
return future; 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.repositories.responses.User;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.UserService; import awais.instagrabber.webservices.UserService;
import kotlinx.coroutines.Dispatchers;
import static awais.instagrabber.utils.Utils.settingsHelper; import static awais.instagrabber.utils.Utils.settingsHelper;
@ -32,7 +33,7 @@ public class AppStateViewModel extends AndroidViewModel {
cookie = settingsHelper.getString(Constants.COOKIE); cookie = settingsHelper.getString(Constants.COOKIE);
final boolean isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != 0; final boolean isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != 0;
if (!isLoggedIn) return; if (!isLoggedIn) return;
userService = UserService.getInstance();
userService = UserService.INSTANCE;
// final AccountRepository accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(application)); // final AccountRepository accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(application));
fetchProfileDetails(); fetchProfileDetails();
} }
@ -49,16 +50,12 @@ public class AppStateViewModel extends AndroidViewModel {
private void fetchProfileDetails() { private void fetchProfileDetails() {
final long uid = CookieUtils.getUserIdFromCookie(cookie); final long uid = CookieUtils.getUserIdFromCookie(cookie);
if (userService == null) return; if (userService == null) return;
userService.getUserInfo(uid, new ServiceCallback<User>() {
@Override
public void onSuccess(final User user) {
currentUser.postValue(user);
userService.getUserInfo(uid, CoroutineUtilsKt.getContinuation((user, throwable) -> {
if (throwable != null) {
Log.e(TAG, "onFailure: ", throwable);
return;
} }
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "onFailure: ", t);
}
});
currentUser.postValue(user);
}, Dispatchers.getIO()));
} }
} }

99
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.repositories.responses.User;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import awais.instagrabber.webservices.CommentService; import awais.instagrabber.webservices.CommentService;
import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.webservices.ServiceCallback; 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; import static awais.instagrabber.utils.Utils.settingsHelper;
@ -113,7 +113,7 @@ public class CommentsViewerViewModel extends ViewModel {
}; };
public CommentsViewerViewModel() { public CommentsViewerViewModel() {
graphQLService = GraphQLService.getInstance();
graphQLService = GraphQLService.INSTANCE;
final String cookie = settingsHelper.getString(Constants.COOKIE); final String cookie = settingsHelper.getString(Constants.COOKIE);
final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID);
final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
@ -165,8 +165,12 @@ public class CommentsViewerViewModel extends ViewModel {
commentService.fetchComments(postId, rootCursor, ccb); commentService.fetchComments(postId, rootCursor, ccb);
return; 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() { public void fetchReplies() {
@ -190,54 +194,49 @@ public class CommentsViewerViewModel extends ViewModel {
commentService.fetchChildComments(postId, commentId, repliesCursor, rcb); commentService.fetchChildComments(postId, commentId, repliesCursor, rcb);
return; return;
} }
final Call<String> request = graphQLService.fetchComments(commentId, false, repliesCursor);
enqueueRequest(request, false, commentId, rcb);
graphQLService.fetchComments(commentId, false, repliesCursor, enqueueRequest(false, commentId, rcb));
} }
private void enqueueRequest(@NonNull final Call<String> request,
final boolean root,
final String shortCodeOrCommentId,
final ServiceCallback callback) {
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
final String rawBody = response.body();
if (rawBody == null) {
Log.e(TAG, "Error occurred while fetching gql comments of " + shortCodeOrCommentId);
callback.onSuccess(null);
return;
}
try {
final JSONObject body = root ? new JSONObject(rawBody).getJSONObject("data")
.getJSONObject("shortcode_media")
.getJSONObject("edge_media_to_parent_comment")
: new JSONObject(rawBody).getJSONObject("data")
.getJSONObject("comment")
.getJSONObject("edge_threaded_comments");
final int count = body.optInt("count");
final JSONObject pageInfo = body.getJSONObject("page_info");
final boolean hasNextPage = pageInfo.getBoolean("has_next_page");
final String endCursor = pageInfo.isNull("end_cursor") || !hasNextPage ? null : pageInfo.optString("end_cursor");
final JSONArray commentsJsonArray = body.getJSONArray("edges");
final ImmutableList.Builder<Comment> builder = ImmutableList.builder();
for (int i = 0; i < commentsJsonArray.length(); i++) {
final Comment commentModel = getComment(commentsJsonArray.getJSONObject(i).getJSONObject("node"), root);
builder.add(commentModel);
}
callback.onSuccess(root ?
new CommentsFetchResponse(count, endCursor, builder.build()) :
new ChildCommentsFetchResponse(count, endCursor, builder.build()));
} catch (Exception e) {
Log.e(TAG, "onResponse", e);
callback.onFailure(e);
}
private Continuation<String> enqueueRequest(final boolean root,
final String shortCodeOrCommentId,
@SuppressWarnings("rawtypes") final ServiceCallback callback) {
return CoroutineUtilsKt.getContinuation((response, throwable) -> {
if (throwable != null) {
callback.onFailure(throwable);
return;
} }
@Override
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) {
callback.onFailure(t);
if (response == null) {
Log.e(TAG, "Error occurred while fetching gql comments of " + shortCodeOrCommentId);
//noinspection unchecked
callback.onSuccess(null);
return;
} }
});
try {
final JSONObject body = root ? new JSONObject(response).getJSONObject("data")
.getJSONObject("shortcode_media")
.getJSONObject("edge_media_to_parent_comment")
: new JSONObject(response).getJSONObject("data")
.getJSONObject("comment")
.getJSONObject("edge_threaded_comments");
final int count = body.optInt("count");
final JSONObject pageInfo = body.getJSONObject("page_info");
final boolean hasNextPage = pageInfo.getBoolean("has_next_page");
final String endCursor = pageInfo.isNull("end_cursor") || !hasNextPage ? null : pageInfo.optString("end_cursor");
final JSONArray commentsJsonArray = body.getJSONArray("edges");
final ImmutableList.Builder<Comment> builder = ImmutableList.builder();
for (int i = 0; i < commentsJsonArray.length(); i++) {
final Comment commentModel = getComment(commentsJsonArray.getJSONObject(i).getJSONObject("node"), root);
builder.add(commentModel);
}
final Object result = root ? new CommentsFetchResponse(count, endCursor, builder.build())
: new ChildCommentsFetchResponse(count, endCursor, builder.build());
//noinspection unchecked
callback.onSuccess(result);
} catch (Exception e) {
Log.e(TAG, "onResponse", e);
callback.onFailure(e);
}
}, Dispatchers.getIO());
} }
@NonNull @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 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( options.add(Option(
if (blocking) getString(R.string.unblock) else getString(R.string.block), if (blocking) getString(R.string.unblock) else getString(R.string.block),
if (blocking) ACTION_UNBLOCK else ACTION_BLOCK if (blocking) ACTION_UNBLOCK else ACTION_BLOCK
@ -144,7 +144,7 @@ class DirectSettingsViewModel(
// options.add(new Option<>(getString(R.string.report), ACTION_REPORT)); // options.add(new Option<>(getString(R.string.report), ACTION_REPORT));
val isGroup: Boolean? = threadManager.isGroup.value val isGroup: Boolean? = threadManager.isGroup.value
if (isGroup != null && isGroup) { if (isGroup != null && isGroup) {
val restricted: Boolean = user.friendshipStatus.isRestricted
val restricted: Boolean = user.friendshipStatus?.isRestricted ?: false
options.add(Option( options.add(Option(
if (restricted) getString(R.string.unrestrict) else getString(R.string.restrict), if (restricted) getString(R.string.unrestrict) else getString(R.string.restrict),
if (restricted) ACTION_UNRESTRICT else ACTION_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 liked = MutableLiveData(false)
private val saved = MutableLiveData(false) private val saved = MutableLiveData(false)
private val options = MutableLiveData<List<Int>>(ArrayList()) 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 lateinit var media: Media
private set private set
private var mediaService: MediaService? = null
private var messageManager: DirectMessagesManager? = null
val isLoggedIn = cookie.isNotBlank() && !csrfToken.isNullOrBlank() && viewerId != 0L
fun setMedia(media: Media) { fun setMedia(media: Media) {
this.media = media this.media = media
@ -125,11 +128,15 @@ class PostViewV2ViewModel : ViewModel() {
fun like(): LiveData<Resource<Any?>> { fun like(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null)) data.postValue(loading(null))
if (!isLoggedIn) {
data.postValue(error("Not logged in!", null))
return data
}
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val mediaId = media.pk ?: return@launch 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) { } catch (e: Exception) {
data.postValue(error(e.message, null)) data.postValue(error(e.message, null))
} }
@ -140,11 +147,15 @@ class PostViewV2ViewModel : ViewModel() {
fun unlike(): LiveData<Resource<Any?>> { fun unlike(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null)) data.postValue(loading(null))
if (!isLoggedIn) {
data.postValue(error("Not logged in!", null))
return data
}
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val mediaId = media.pk ?: return@launch 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) { } catch (e: Exception) {
data.postValue(error(e.message, null)) data.postValue(error(e.message, null))
} }
@ -185,11 +196,15 @@ class PostViewV2ViewModel : ViewModel() {
fun save(collection: String?, ignoreSaveState: Boolean): LiveData<Resource<Any?>> { fun save(collection: String?, ignoreSaveState: Boolean): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null)) data.postValue(loading(null))
if (!isLoggedIn) {
data.postValue(error("Not logged in!", null))
return data
}
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val mediaId = media.pk ?: return@launch 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) { } catch (e: Exception) {
data.postValue(error(e.message, null)) data.postValue(error(e.message, null))
} }
@ -200,10 +215,14 @@ class PostViewV2ViewModel : ViewModel() {
fun unsave(): LiveData<Resource<Any?>> { fun unsave(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null)) data.postValue(loading(null))
if (!isLoggedIn) {
data.postValue(error("Not logged in!", null))
return data
}
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val mediaId = media.pk ?: return@launch 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 return data
} }
@ -225,11 +244,15 @@ class PostViewV2ViewModel : ViewModel() {
fun updateCaption(caption: String): LiveData<Resource<Any?>> { fun updateCaption(caption: String): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null)) data.postValue(loading(null))
if (!isLoggedIn) {
data.postValue(error("Not logged in!", null))
return data
}
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val postId = media.pk ?: return@launch 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("")) data.postValue(success(""))
media.setPostCaption(caption) media.setPostCaption(caption)
this@PostViewV2ViewModel.caption.postValue(media.caption) this@PostViewV2ViewModel.caption.postValue(media.caption)
@ -255,8 +278,8 @@ class PostViewV2ViewModel : ViewModel() {
} }
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val result = mediaService?.translate(pk, "1")
if (result.isNullOrBlank()) {
val result = MediaService.translate(pk, "1")
if (result.isBlank()) {
data.postValue(error("", null)) data.postValue(error("", null))
return@launch return@launch
} }
@ -280,6 +303,10 @@ class PostViewV2ViewModel : ViewModel() {
fun delete(): LiveData<Resource<Any?>> { fun delete(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null)) data.postValue(loading(null))
if (!isLoggedIn) {
data.postValue(error("Not logged in!", null))
return data
}
val mediaId = media.id val mediaId = media.id
val mediaType = media.mediaType val mediaType = media.mediaType
if (mediaId == null || mediaType == null) { if (mediaId == null || mediaType == null) {
@ -288,7 +315,7 @@ class PostViewV2ViewModel : ViewModel() {
} }
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val response = mediaService?.delete(mediaId, mediaType)
val response = MediaService.delete(csrfToken!!, viewerId, deviceUuid, mediaId, mediaType)
if (response == null) { if (response == null) {
data.postValue(success(Any())) data.postValue(success(Any()))
return@launch return@launch
@ -317,15 +344,4 @@ class PostViewV2ViewModel : ViewModel() {
val mediaId = media.id ?: return val mediaId = media.id ?: return
messageManager?.sendMedia(recipients, mediaId, viewModelScope) 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())
}
}

62
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.fragments.UserSearchFragment;
import awais.instagrabber.models.Resource; import awais.instagrabber.models.Resource;
import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.UserSearchResponse;
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; import awais.instagrabber.repositories.responses.directmessages.RankedRecipient;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
@ -37,7 +36,6 @@ import awais.instagrabber.webservices.UserService;
import kotlinx.coroutines.Dispatchers; import kotlinx.coroutines.Dispatchers;
import okhttp3.ResponseBody; import okhttp3.ResponseBody;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
import static awais.instagrabber.utils.Utils.settingsHelper; 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)) { if (TextUtils.isEmpty(csrfToken) || viewerId <= 0 || TextUtils.isEmpty(deviceUuid)) {
throw new IllegalArgumentException("User is not logged in!"); 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; rankedRecipientsCache = RankedRecipientsCache.INSTANCE;
if ((rankedRecipientsCache.isFailed() || rankedRecipientsCache.isExpired()) && !rankedRecipientsCache.isUpdateInitiated()) { if ((rankedRecipientsCache.isFailed() || rankedRecipientsCache.isExpired()) && !rankedRecipientsCache.isUpdateInitiated()) {
updateRankedRecipientCache(); updateRankedRecipientCache();
@ -170,9 +168,26 @@ public class UserSearchViewModel extends ViewModel {
} }
private void defaultUserSearch() { private void defaultUserSearch() {
searchRequest = userService.search(currentQuery);
//noinspection unchecked
handleRequest((Call<UserSearchResponse>) searchRequest);
userService.search(currentQuery, CoroutineUtilsKt.getContinuation((userSearchResponse, throwable) -> {
if (throwable != null) {
Log.e(TAG, "onFailure: ", throwable);
recipients.postValue(Resource.error(throwable.getMessage(), getCachedRecipients()));
searchRequest = null;
return;
}
if (userSearchResponse == null) {
recipients.postValue(Resource.error(R.string.generic_null_response, getCachedRecipients()));
searchRequest = null;
return;
}
final List<RankedRecipient> list = userSearchResponse
.getUsers()
.stream()
.map(RankedRecipient::of)
.collect(Collectors.toList());
recipients.postValue(Resource.success(mergeResponseWithCache(list)));
searchRequest = null;
}));
} }
private void rankedRecipientSearch() { private void rankedRecipientSearch() {
@ -194,39 +209,6 @@ public class UserSearchViewModel extends ViewModel {
); );
} }
private void handleRequest(@NonNull final Call<UserSearchResponse> request) {
request.enqueue(new Callback<UserSearchResponse>() {
@Override
public void onResponse(@NonNull final Call<UserSearchResponse> call, @NonNull final Response<UserSearchResponse> response) {
if (!response.isSuccessful()) {
handleErrorResponse(response, true);
searchRequest = null;
return;
}
final UserSearchResponse userSearchResponse = response.body();
if (userSearchResponse == null) {
recipients.postValue(Resource.error(R.string.generic_null_response, getCachedRecipients()));
searchRequest = null;
return;
}
final List<RankedRecipient> list = userSearchResponse
.getUsers()
.stream()
.map(RankedRecipient::of)
.collect(Collectors.toList());
recipients.postValue(Resource.success(mergeResponseWithCache(list)));
searchRequest = null;
}
@Override
public void onFailure(@NonNull final Call<UserSearchResponse> call, @NonNull final Throwable t) {
Log.e(TAG, "onFailure: ", t);
recipients.postValue(Resource.error(t.getMessage(), getCachedRecipients()));
searchRequest = null;
}
});
}
private List<RankedRecipient> mergeResponseWithCache(@NonNull final List<RankedRecipient> list) { private List<RankedRecipient> mergeResponseWithCache(@NonNull final List<RankedRecipient> list) {
final Iterator<RankedRecipient> iterator = list.stream() final Iterator<RankedRecipient> iterator = list.stream()
.filter(Objects::nonNull) .filter(Objects::nonNull)

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.directmessages.*
import awais.instagrabber.repositories.responses.giphy.GiphyGif import awais.instagrabber.repositories.responses.giphy.GiphyGif
import awais.instagrabber.utils.TextUtils.extractUrls import awais.instagrabber.utils.TextUtils.extractUrls
import awais.instagrabber.utils.TextUtils.isEmpty
import awais.instagrabber.utils.Utils import awais.instagrabber.utils.Utils
import org.json.JSONArray import org.json.JSONArray
import java.util.* 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) private val repository: DirectMessagesRepository = RetrofitFactory.retrofit.create(DirectMessagesRepository::class.java)
suspend fun fetchInbox( suspend fun fetchInbox(
@ -55,6 +50,9 @@ class DirectMessagesService private constructor(
suspend fun fetchUnseenCount(): DirectBadgeCount = repository.fetchUnseenCount() suspend fun fetchUnseenCount(): DirectBadgeCount = repository.fetchUnseenCount()
suspend fun broadcastText( suspend fun broadcastText(
csrfToken: String,
userId: Long,
deviceUuid: String,
clientContext: String, clientContext: String,
threadIdOrUserIds: ThreadIdOrUserIds, threadIdOrUserIds: ThreadIdOrUserIds,
text: String, text: String,
@ -63,17 +61,20 @@ class DirectMessagesService private constructor(
): DirectThreadBroadcastResponse { ): DirectThreadBroadcastResponse {
val urls = extractUrls(text) val urls = extractUrls(text)
if (urls.isNotEmpty()) { 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) val broadcastOptions = TextBroadcastOptions(clientContext, threadIdOrUserIds, text)
if (!repliedToItemId.isNullOrBlank() && !repliedToClientContext.isNullOrBlank()) { if (!repliedToItemId.isNullOrBlank() && !repliedToClientContext.isNullOrBlank()) {
broadcastOptions.repliedToItemId = repliedToItemId broadcastOptions.repliedToItemId = repliedToItemId
broadcastOptions.repliedToClientContext = repliedToClientContext broadcastOptions.repliedToClientContext = repliedToClientContext
} }
return broadcast(broadcastOptions)
return broadcast(csrfToken, userId, deviceUuid, broadcastOptions)
} }
private suspend fun broadcastLink( private suspend fun broadcastLink(
csrfToken: String,
userId: Long,
deviceUuid: String,
clientContext: String, clientContext: String,
threadIdOrUserIds: ThreadIdOrUserIds, threadIdOrUserIds: ThreadIdOrUserIds,
linkText: String, linkText: String,
@ -86,75 +87,100 @@ class DirectMessagesService private constructor(
broadcastOptions.repliedToItemId = repliedToItemId broadcastOptions.repliedToItemId = repliedToItemId
broadcastOptions.repliedToClientContext = repliedToClientContext broadcastOptions.repliedToClientContext = repliedToClientContext
} }
return broadcast(broadcastOptions)
return broadcast(csrfToken, userId, deviceUuid, broadcastOptions)
} }
suspend fun broadcastPhoto( suspend fun broadcastPhoto(
csrfToken: String,
userId: Long,
deviceUuid: String,
clientContext: String, clientContext: String,
threadIdOrUserIds: ThreadIdOrUserIds, threadIdOrUserIds: ThreadIdOrUserIds,
uploadId: String, uploadId: String,
): DirectThreadBroadcastResponse {
return broadcast(PhotoBroadcastOptions(clientContext, threadIdOrUserIds, true, uploadId))
}
): DirectThreadBroadcastResponse =
broadcast(csrfToken, userId, deviceUuid, PhotoBroadcastOptions(clientContext, threadIdOrUserIds, true, uploadId))
suspend fun broadcastVideo( suspend fun broadcastVideo(
csrfToken: String,
userId: Long,
deviceUuid: String,
clientContext: String, clientContext: String,
threadIdOrUserIds: ThreadIdOrUserIds, threadIdOrUserIds: ThreadIdOrUserIds,
uploadId: String, uploadId: String,
videoResult: String, videoResult: String,
sampled: Boolean, 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( suspend fun broadcastVoice(
csrfToken: String,
userId: Long,
deviceUuid: String,
clientContext: String, clientContext: String,
threadIdOrUserIds: ThreadIdOrUserIds, threadIdOrUserIds: ThreadIdOrUserIds,
uploadId: String, uploadId: String,
waveform: List<Float>, waveform: List<Float>,
samplingFreq: Int, 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( suspend fun broadcastStoryReply(
csrfToken: String,
userId: Long,
deviceUuid: String,
threadIdOrUserIds: ThreadIdOrUserIds, threadIdOrUserIds: ThreadIdOrUserIds,
text: String, text: String,
mediaId: String, mediaId: String,
reelId: 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( suspend fun broadcastReaction(
csrfToken: String,
userId: Long,
deviceUuid: String,
clientContext: String, clientContext: String,
threadIdOrUserIds: ThreadIdOrUserIds, threadIdOrUserIds: ThreadIdOrUserIds,
itemId: String, itemId: String,
emoji: String?, emoji: String?,
delete: Boolean, 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( suspend fun broadcastAnimatedMedia(
csrfToken: String,
userId: Long,
deviceUuid: String,
clientContext: String, clientContext: String,
threadIdOrUserIds: ThreadIdOrUserIds, threadIdOrUserIds: ThreadIdOrUserIds,
giphyGif: GiphyGif, giphyGif: GiphyGif,
): DirectThreadBroadcastResponse {
return broadcast(AnimatedMediaBroadcastOptions(clientContext, threadIdOrUserIds, giphyGif))
}
): DirectThreadBroadcastResponse =
broadcast(csrfToken, userId, deviceUuid, AnimatedMediaBroadcastOptions(clientContext, threadIdOrUserIds, giphyGif))
suspend fun broadcastMediaShare( suspend fun broadcastMediaShare(
csrfToken: String,
userId: Long,
deviceUuid: String,
clientContext: String, clientContext: String,
threadIdOrUserIds: ThreadIdOrUserIds, threadIdOrUserIds: ThreadIdOrUserIds,
mediaId: String, mediaId: String,
): DirectThreadBroadcastResponse =
broadcast(csrfToken, userId, deviceUuid, MediaShareBroadcastOptions(clientContext, threadIdOrUserIds, mediaId))
private suspend fun broadcast(
csrfToken: String,
userId: Long,
deviceUuid: String,
broadcastOptions: BroadcastOptions,
): DirectThreadBroadcastResponse { ): DirectThreadBroadcastResponse {
return broadcast(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>()
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 val threadId = broadcastOptions.threadId
if (!threadId.isNullOrBlank()) { if (!threadId.isNullOrBlank()) {
form["thread_id"] = threadId form["thread_id"] = threadId
@ -165,11 +191,6 @@ class DirectMessagesService private constructor(
} }
form["recipient_users"] = JSONArray(userIds).toString() 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 repliedToItemId = broadcastOptions.repliedToItemId
val repliedToClientContext = broadcastOptions.repliedToClientContext val repliedToClientContext = broadcastOptions.repliedToClientContext
if (!repliedToItemId.isNullOrBlank() && !repliedToClientContext.isNullOrBlank()) { if (!repliedToItemId.isNullOrBlank() && !repliedToClientContext.isNullOrBlank()) {
@ -183,6 +204,8 @@ class DirectMessagesService private constructor(
} }
suspend fun addUsers( suspend fun addUsers(
csrfToken: String,
deviceUuid: String,
threadId: String, threadId: String,
userIds: Collection<Long>, userIds: Collection<Long>,
): DirectThreadDetailsChangeResponse { ): DirectThreadDetailsChangeResponse {
@ -195,6 +218,8 @@ class DirectMessagesService private constructor(
} }
suspend fun removeUsers( suspend fun removeUsers(
csrfToken: String,
deviceUuid: String,
threadId: String, threadId: String,
userIds: Collection<Long>, userIds: Collection<Long>,
): String { ): String {
@ -207,6 +232,8 @@ class DirectMessagesService private constructor(
} }
suspend fun updateTitle( suspend fun updateTitle(
csrfToken: String,
deviceUuid: String,
threadId: String, threadId: String,
title: String, title: String,
): DirectThreadDetailsChangeResponse { ): DirectThreadDetailsChangeResponse {
@ -219,6 +246,8 @@ class DirectMessagesService private constructor(
} }
suspend fun addAdmins( suspend fun addAdmins(
csrfToken: String,
deviceUuid: String,
threadId: String, threadId: String,
userIds: Collection<Long>, userIds: Collection<Long>,
): String { ): String {
@ -231,6 +260,8 @@ class DirectMessagesService private constructor(
} }
suspend fun removeAdmins( suspend fun removeAdmins(
csrfToken: String,
deviceUuid: String,
threadId: String, threadId: String,
userIds: Collection<Long>, userIds: Collection<Long>,
): String { ): String {
@ -243,6 +274,8 @@ class DirectMessagesService private constructor(
} }
suspend fun deleteItem( suspend fun deleteItem(
csrfToken: String,
deviceUuid: String,
threadId: String, threadId: String,
itemId: String, itemId: String,
): String { ): String {
@ -292,6 +325,9 @@ class DirectMessagesService private constructor(
} }
suspend fun createThread( suspend fun createThread(
csrfToken: String,
userId: Long,
deviceUuid: String,
userIds: List<Long>, userIds: List<Long>,
threadTitle: String?, threadTitle: String?,
): DirectThread { ): DirectThread {
@ -309,7 +345,11 @@ class DirectMessagesService private constructor(
return repository.createThread(signedForm) return repository.createThread(signedForm)
} }
suspend fun mute(threadId: String): String {
suspend fun mute(
csrfToken: String,
deviceUuid: String,
threadId: String,
): String {
val form = mapOf( val form = mapOf(
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid "_uuid" to deviceUuid
@ -317,7 +357,11 @@ class DirectMessagesService private constructor(
return repository.mute(threadId, form) return repository.mute(threadId, form)
} }
suspend fun unmute(threadId: String): String {
suspend fun unmute(
csrfToken: String,
deviceUuid: String,
threadId: String,
): String {
val form = mapOf( val form = mapOf(
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
@ -325,7 +369,11 @@ class DirectMessagesService private constructor(
return repository.unmute(threadId, form) return repository.unmute(threadId, form)
} }
suspend fun muteMentions(threadId: String): String {
suspend fun muteMentions(
csrfToken: String,
deviceUuid: String,
threadId: String,
): String {
val form = mapOf( val form = mapOf(
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
@ -333,7 +381,11 @@ class DirectMessagesService private constructor(
return repository.muteMentions(threadId, form) return repository.muteMentions(threadId, form)
} }
suspend fun unmuteMentions(threadId: String): String {
suspend fun unmuteMentions(
csrfToken: String,
deviceUuid: String,
threadId: String,
): String {
val form = mapOf( val form = mapOf(
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
@ -350,6 +402,8 @@ class DirectMessagesService private constructor(
} }
suspend fun approveParticipantRequests( suspend fun approveParticipantRequests(
csrfToken: String,
deviceUuid: String,
threadId: String, threadId: String,
userIds: List<Long>, userIds: List<Long>,
): DirectThreadDetailsChangeResponse { ): DirectThreadDetailsChangeResponse {
@ -363,6 +417,8 @@ class DirectMessagesService private constructor(
} }
suspend fun declineParticipantRequests( suspend fun declineParticipantRequests(
csrfToken: String,
deviceUuid: String,
threadId: String, threadId: String,
userIds: List<Long>, userIds: List<Long>,
): DirectThreadDetailsChangeResponse { ): DirectThreadDetailsChangeResponse {
@ -374,7 +430,11 @@ class DirectMessagesService private constructor(
return repository.declineParticipantRequests(threadId, form) return repository.declineParticipantRequests(threadId, form)
} }
suspend fun approvalRequired(threadId: String): DirectThreadDetailsChangeResponse {
suspend fun approvalRequired(
csrfToken: String,
deviceUuid: String,
threadId: String,
): DirectThreadDetailsChangeResponse {
val form = mapOf( val form = mapOf(
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
@ -382,7 +442,11 @@ class DirectMessagesService private constructor(
return repository.approvalRequired(threadId, form) return repository.approvalRequired(threadId, form)
} }
suspend fun approvalNotRequired(threadId: String): DirectThreadDetailsChangeResponse {
suspend fun approvalNotRequired(
csrfToken: String,
deviceUuid: String,
threadId: String,
): DirectThreadDetailsChangeResponse {
val form = mapOf( val form = mapOf(
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
@ -390,7 +454,11 @@ class DirectMessagesService private constructor(
return repository.approvalNotRequired(threadId, form) return repository.approvalNotRequired(threadId, form)
} }
suspend fun leave(threadId: String): DirectThreadDetailsChangeResponse {
suspend fun leave(
csrfToken: String,
deviceUuid: String,
threadId: String,
): DirectThreadDetailsChangeResponse {
val form = mapOf( val form = mapOf(
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
@ -398,7 +466,11 @@ class DirectMessagesService private constructor(
return repository.leave(threadId, form) return repository.leave(threadId, form)
} }
suspend fun end(threadId: String): DirectThreadDetailsChangeResponse {
suspend fun end(
csrfToken: String,
deviceUuid: String,
threadId: String,
): DirectThreadDetailsChangeResponse {
val form = mapOf( val form = mapOf(
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
@ -423,7 +495,11 @@ class DirectMessagesService private constructor(
return repository.fetchPendingInbox(queryMap) return repository.fetchPendingInbox(queryMap)
} }
suspend fun approveRequest(threadId: String): String {
suspend fun approveRequest(
csrfToken: String,
deviceUuid: String,
threadId: String,
): String {
val form = mapOf( val form = mapOf(
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
@ -431,7 +507,11 @@ class DirectMessagesService private constructor(
return repository.approveRequest(threadId, form) return repository.approveRequest(threadId, form)
} }
suspend fun declineRequest(threadId: String): String {
suspend fun declineRequest(
csrfToken: String,
deviceUuid: String,
threadId: String,
): String {
val form = mapOf( val form = mapOf(
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
@ -440,6 +520,8 @@ class DirectMessagesService private constructor(
} }
suspend fun markAsSeen( suspend fun markAsSeen(
csrfToken: String,
deviceUuid: String,
threadId: String, threadId: String,
directItem: DirectItem, directItem: DirectItem,
): DirectItemSeenResponse? { ): DirectItemSeenResponse? {
@ -454,25 +536,4 @@ class DirectMessagesService private constructor(
) )
return repository.markItemSeen(threadId, itemId, form) 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 awais.instagrabber.webservices.RetrofitFactory.retrofit
import org.json.JSONObject 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) private val repository: MediaRepository = retrofit.create(MediaRepository::class.java)
suspend fun fetch( suspend fun fetch(
@ -28,15 +29,38 @@ class MediaService private constructor(
} else response.items[0] } 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( private suspend fun action(
csrfToken: String,
userId: Long,
deviceUuid: String,
mediaId: String, mediaId: String,
action: String, action: String,
collection: String?, collection: String?,
@ -60,6 +84,9 @@ class MediaService private constructor(
} }
suspend fun editCaption( suspend fun editCaption(
csrfToken: String,
userId: Long,
deviceUuid: String,
postId: String, postId: String,
newCaption: String, newCaption: String,
): Boolean { ): Boolean {
@ -99,7 +126,12 @@ class MediaService private constructor(
return jsonObject.optString("translation") 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) { if (options.videoOptions != null) {
val videoOptions = options.videoOptions val videoOptions = options.videoOptions
if (videoOptions.clips.isEmpty()) { if (videoOptions.clips.isEmpty()) {
@ -124,6 +156,9 @@ class MediaService private constructor(
} }
suspend fun delete( suspend fun delete(
csrfToken: String,
userId: Long,
deviceUuid: String,
postId: String, postId: String,
type: MediaItemType, type: MediaItemType,
): String? { ): String? {
@ -144,26 +179,4 @@ class MediaService private constructor(
} }
return repository.delete(postId, mediaType, signedForm) 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