Browse Source
Merge branch 'master' of https://github.com/austinhuang0131/instagrabber
renovate/org.robolectric-robolectric-4.x
Merge branch 'master' of https://github.com/austinhuang0131/instagrabber
renovate/org.robolectric-robolectric-4.x
Austin Huang
4 years ago
No known key found for this signature in database
GPG Key ID: 84C23AA04587A91F
58 changed files with 2410 additions and 3089 deletions
-
9app/build.gradle
-
49app/src/main/java/awais/instagrabber/activities/MainActivity.kt
-
16app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java
-
16app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java
-
18app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java
-
17app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java
-
34app/src/main/java/awais/instagrabber/db/dao/AccountDao.java
-
25app/src/main/java/awais/instagrabber/db/dao/AccountDao.kt
-
68app/src/main/java/awais/instagrabber/db/datasources/AccountDataSource.java
-
49app/src/main/java/awais/instagrabber/db/datasources/AccountDataSource.kt
-
131app/src/main/java/awais/instagrabber/db/repositories/AccountRepository.java
-
49app/src/main/java/awais/instagrabber/db/repositories/AccountRepository.kt
-
62app/src/main/java/awais/instagrabber/dialogs/AccountSwitcherDialogFragment.java
-
41app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java
-
138app/src/main/java/awais/instagrabber/fragments/FollowViewerFragment.java
-
65app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java
-
33app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java
-
69app/src/main/java/awais/instagrabber/fragments/LocationFragment.java
-
60app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java
-
44app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java
-
253app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java
-
4app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.kt
-
12app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java
-
40app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java
-
332app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java
-
235app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java
-
17app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.kt
-
19app/src/main/java/awais/instagrabber/managers/InboxManager.kt
-
217app/src/main/java/awais/instagrabber/managers/ThreadManager.kt
-
37app/src/main/java/awais/instagrabber/repositories/FriendshipRepository.java
-
36app/src/main/java/awais/instagrabber/repositories/FriendshipRepository.kt
-
25app/src/main/java/awais/instagrabber/repositories/GraphQLRepository.java
-
22app/src/main/java/awais/instagrabber/repositories/GraphQLRepository.kt
-
43app/src/main/java/awais/instagrabber/repositories/StoriesRepository.java
-
38app/src/main/java/awais/instagrabber/repositories/StoriesRepository.kt
-
25app/src/main/java/awais/instagrabber/repositories/UserRepository.java
-
25app/src/main/java/awais/instagrabber/repositories/UserRepository.kt
-
296app/src/main/java/awais/instagrabber/repositories/responses/User.java
-
38app/src/main/java/awais/instagrabber/repositories/responses/User.kt
-
10app/src/main/java/awais/instagrabber/utils/CookieUtils.kt
-
50app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java
-
21app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java
-
99app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java
-
4app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.kt
-
72app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt
-
21app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt
-
62app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java
-
199app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.kt
-
264app/src/main/java/awais/instagrabber/webservices/FriendshipService.java
-
155app/src/main/java/awais/instagrabber/webservices/FriendshipService.kt
-
483app/src/main/java/awais/instagrabber/webservices/GraphQLService.java
-
266app/src/main/java/awais/instagrabber/webservices/GraphQLService.kt
-
81app/src/main/java/awais/instagrabber/webservices/MediaService.kt
-
548app/src/main/java/awais/instagrabber/webservices/StoriesService.java
-
309app/src/main/java/awais/instagrabber/webservices/StoriesService.kt
-
101app/src/main/java/awais/instagrabber/webservices/UserService.java
-
29app/src/main/java/awais/instagrabber/webservices/UserService.kt
-
18app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt
@ -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(); |
|
||||
} |
|
@ -0,0 +1,25 @@ |
|||||
|
package awais.instagrabber.db.dao |
||||
|
|
||||
|
import androidx.room.* |
||||
|
import awais.instagrabber.db.entities.Account |
||||
|
|
||||
|
@Dao |
||||
|
interface AccountDao { |
||||
|
@Query("SELECT * FROM accounts") |
||||
|
suspend fun getAllAccounts(): List<Account> |
||||
|
|
||||
|
@Query("SELECT * FROM accounts WHERE uid = :uid") |
||||
|
suspend fun findAccountByUid(uid: String): Account? |
||||
|
|
||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE) |
||||
|
suspend fun insertAccounts(vararg accounts: Account): List<Long> |
||||
|
|
||||
|
@Update |
||||
|
suspend fun updateAccounts(vararg accounts: Account) |
||||
|
|
||||
|
@Delete |
||||
|
suspend fun deleteAccounts(vararg accounts: Account) |
||||
|
|
||||
|
@Query("DELETE from accounts") |
||||
|
suspend fun deleteAllAccounts() |
||||
|
} |
@ -1,68 +0,0 @@ |
|||||
package awais.instagrabber.db.datasources; |
|
||||
|
|
||||
import android.content.Context; |
|
||||
|
|
||||
import androidx.annotation.NonNull; |
|
||||
import androidx.annotation.Nullable; |
|
||||
|
|
||||
import java.util.List; |
|
||||
|
|
||||
import awais.instagrabber.db.AppDatabase; |
|
||||
import awais.instagrabber.db.dao.AccountDao; |
|
||||
import awais.instagrabber.db.entities.Account; |
|
||||
|
|
||||
public class AccountDataSource { |
|
||||
private static final String TAG = AccountDataSource.class.getSimpleName(); |
|
||||
|
|
||||
private static AccountDataSource INSTANCE; |
|
||||
|
|
||||
private final AccountDao accountDao; |
|
||||
|
|
||||
private AccountDataSource(final AccountDao accountDao) { |
|
||||
this.accountDao = accountDao; |
|
||||
} |
|
||||
|
|
||||
public static AccountDataSource getInstance(@NonNull Context context) { |
|
||||
if (INSTANCE == null) { |
|
||||
synchronized (AccountDataSource.class) { |
|
||||
if (INSTANCE == null) { |
|
||||
final AppDatabase database = AppDatabase.getDatabase(context); |
|
||||
INSTANCE = new AccountDataSource(database.accountDao()); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
return INSTANCE; |
|
||||
} |
|
||||
|
|
||||
@Nullable |
|
||||
public final Account getAccount(final String uid) { |
|
||||
return accountDao.findAccountByUid(uid); |
|
||||
} |
|
||||
|
|
||||
@NonNull |
|
||||
public final List<Account> getAllAccounts() { |
|
||||
return accountDao.getAllAccounts(); |
|
||||
} |
|
||||
|
|
||||
public final void insertOrUpdateAccount(final String uid, |
|
||||
final String username, |
|
||||
final String cookie, |
|
||||
final String fullName, |
|
||||
final String profilePicUrl) { |
|
||||
final Account account = getAccount(uid); |
|
||||
final Account toUpdate = new Account(account == null ? 0 : account.getId(), uid, username, cookie, fullName, profilePicUrl); |
|
||||
if (account != null) { |
|
||||
accountDao.updateAccounts(toUpdate); |
|
||||
return; |
|
||||
} |
|
||||
accountDao.insertAccounts(toUpdate); |
|
||||
} |
|
||||
|
|
||||
public final void deleteAccount(@NonNull final Account account) { |
|
||||
accountDao.deleteAccounts(account); |
|
||||
} |
|
||||
|
|
||||
public final void deleteAllAccounts() { |
|
||||
accountDao.deleteAllAccounts(); |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,49 @@ |
|||||
|
package awais.instagrabber.db.datasources |
||||
|
|
||||
|
import android.content.Context |
||||
|
import awais.instagrabber.db.AppDatabase |
||||
|
import awais.instagrabber.db.dao.AccountDao |
||||
|
import awais.instagrabber.db.entities.Account |
||||
|
|
||||
|
class AccountDataSource private constructor(private val accountDao: AccountDao) { |
||||
|
suspend fun getAccount(uid: String): Account? = accountDao.findAccountByUid(uid) |
||||
|
|
||||
|
suspend fun getAllAccounts(): List<Account> = accountDao.getAllAccounts() |
||||
|
|
||||
|
suspend fun insertOrUpdateAccount( |
||||
|
uid: String, |
||||
|
username: String, |
||||
|
cookie: String, |
||||
|
fullName: String, |
||||
|
profilePicUrl: String?, |
||||
|
) { |
||||
|
val account = getAccount(uid) |
||||
|
val toUpdate = Account(account?.id ?: 0, uid, username, cookie, fullName, profilePicUrl) |
||||
|
if (account != null) { |
||||
|
accountDao.updateAccounts(toUpdate) |
||||
|
return |
||||
|
} |
||||
|
accountDao.insertAccounts(toUpdate) |
||||
|
} |
||||
|
|
||||
|
suspend fun deleteAccount(account: Account) = accountDao.deleteAccounts(account) |
||||
|
|
||||
|
suspend fun deleteAllAccounts() = accountDao.deleteAllAccounts() |
||||
|
|
||||
|
companion object { |
||||
|
private lateinit var INSTANCE: AccountDataSource |
||||
|
|
||||
|
@JvmStatic |
||||
|
fun getInstance(context: Context): AccountDataSource { |
||||
|
if (!this::INSTANCE.isInitialized) { |
||||
|
synchronized(AccountDataSource::class.java) { |
||||
|
if (!this::INSTANCE.isInitialized) { |
||||
|
val database = AppDatabase.getDatabase(context) |
||||
|
INSTANCE = AccountDataSource(database.accountDao()) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return INSTANCE |
||||
|
} |
||||
|
} |
||||
|
} |
@ -1,131 +0,0 @@ |
|||||
package awais.instagrabber.db.repositories; |
|
||||
|
|
||||
import java.util.List; |
|
||||
|
|
||||
import awais.instagrabber.db.datasources.AccountDataSource; |
|
||||
import awais.instagrabber.db.entities.Account; |
|
||||
import awais.instagrabber.utils.AppExecutors; |
|
||||
|
|
||||
public class AccountRepository { |
|
||||
private static final String TAG = AccountRepository.class.getSimpleName(); |
|
||||
|
|
||||
private static AccountRepository instance; |
|
||||
|
|
||||
private final AppExecutors appExecutors; |
|
||||
private final AccountDataSource accountDataSource; |
|
||||
|
|
||||
// private List<Account> cachedAccounts; |
|
||||
|
|
||||
private AccountRepository(final AppExecutors appExecutors, final AccountDataSource accountDataSource) { |
|
||||
this.appExecutors = appExecutors; |
|
||||
this.accountDataSource = accountDataSource; |
|
||||
} |
|
||||
|
|
||||
public static AccountRepository getInstance(final AccountDataSource accountDataSource) { |
|
||||
if (instance == null) { |
|
||||
instance = new AccountRepository(AppExecutors.INSTANCE, accountDataSource); |
|
||||
} |
|
||||
return instance; |
|
||||
} |
|
||||
|
|
||||
public void getAccount(final long uid, |
|
||||
final RepositoryCallback<Account> callback) { |
|
||||
// request on the I/O thread |
|
||||
appExecutors.getDiskIO().execute(() -> { |
|
||||
final Account account = accountDataSource.getAccount(String.valueOf(uid)); |
|
||||
// notify on the main thread |
|
||||
appExecutors.getMainThread().execute(() -> { |
|
||||
if (callback == null) return; |
|
||||
if (account == null) { |
|
||||
callback.onDataNotAvailable(); |
|
||||
return; |
|
||||
} |
|
||||
callback.onSuccess(account); |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public void getAllAccounts(final RepositoryCallback<List<Account>> callback) { |
|
||||
// request on the I/O thread |
|
||||
appExecutors.getDiskIO().execute(() -> { |
|
||||
final List<Account> accounts = accountDataSource.getAllAccounts(); |
|
||||
// notify on the main thread |
|
||||
appExecutors.getMainThread().execute(() -> { |
|
||||
if (callback == null) return; |
|
||||
if (accounts == null) { |
|
||||
callback.onDataNotAvailable(); |
|
||||
return; |
|
||||
} |
|
||||
// cachedAccounts = accounts; |
|
||||
callback.onSuccess(accounts); |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public void insertOrUpdateAccounts(final List<Account> accounts, |
|
||||
final RepositoryCallback<Void> callback) { |
|
||||
// request on the I/O thread |
|
||||
appExecutors.getDiskIO().execute(() -> { |
|
||||
for (final Account account : accounts) { |
|
||||
accountDataSource.insertOrUpdateAccount(account.getUid(), |
|
||||
account.getUsername(), |
|
||||
account.getCookie(), |
|
||||
account.getFullName(), |
|
||||
account.getProfilePic()); |
|
||||
} |
|
||||
// notify on the main thread |
|
||||
appExecutors.getMainThread().execute(() -> { |
|
||||
if (callback == null) return; |
|
||||
callback.onSuccess(null); |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public void insertOrUpdateAccount(final long uid, |
|
||||
final String username, |
|
||||
final String cookie, |
|
||||
final String fullName, |
|
||||
final String profilePicUrl, |
|
||||
final RepositoryCallback<Account> callback) { |
|
||||
// request on the I/O thread |
|
||||
appExecutors.getDiskIO().execute(() -> { |
|
||||
accountDataSource.insertOrUpdateAccount(String.valueOf(uid), username, cookie, fullName, profilePicUrl); |
|
||||
final Account updated = accountDataSource.getAccount(String.valueOf(uid)); |
|
||||
// notify on the main thread |
|
||||
appExecutors.getMainThread().execute(() -> { |
|
||||
if (callback == null) return; |
|
||||
if (updated == null) { |
|
||||
callback.onDataNotAvailable(); |
|
||||
return; |
|
||||
} |
|
||||
callback.onSuccess(updated); |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public void deleteAccount(final Account account, |
|
||||
final RepositoryCallback<Void> callback) { |
|
||||
// request on the I/O thread |
|
||||
appExecutors.getDiskIO().execute(() -> { |
|
||||
accountDataSource.deleteAccount(account); |
|
||||
// notify on the main thread |
|
||||
appExecutors.getMainThread().execute(() -> { |
|
||||
if (callback == null) return; |
|
||||
callback.onSuccess(null); |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public void deleteAllAccounts(final RepositoryCallback<Void> callback) { |
|
||||
// request on the I/O thread |
|
||||
appExecutors.getDiskIO().execute(() -> { |
|
||||
accountDataSource.deleteAllAccounts(); |
|
||||
// notify on the main thread |
|
||||
appExecutors.getMainThread().execute(() -> { |
|
||||
if (callback == null) return; |
|
||||
callback.onSuccess(null); |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
} |
|
@ -0,0 +1,49 @@ |
|||||
|
package awais.instagrabber.db.repositories |
||||
|
|
||||
|
import awais.instagrabber.db.datasources.AccountDataSource |
||||
|
import awais.instagrabber.db.entities.Account |
||||
|
|
||||
|
class AccountRepository private constructor(private val accountDataSource: AccountDataSource) { |
||||
|
suspend fun getAccount(uid: Long): Account? = accountDataSource.getAccount(uid.toString()) |
||||
|
|
||||
|
suspend fun getAllAccounts(): List<Account> = accountDataSource.getAllAccounts() |
||||
|
|
||||
|
suspend fun insertOrUpdateAccounts(accounts: List<Account>) { |
||||
|
for (account in accounts) { |
||||
|
accountDataSource.insertOrUpdateAccount( |
||||
|
account.uid, |
||||
|
account.username, |
||||
|
account.cookie, |
||||
|
account.fullName, |
||||
|
account.profilePic |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
suspend fun insertOrUpdateAccount( |
||||
|
uid: Long, |
||||
|
username: String, |
||||
|
cookie: String, |
||||
|
fullName: String, |
||||
|
profilePicUrl: String?, |
||||
|
): Account? { |
||||
|
accountDataSource.insertOrUpdateAccount(uid.toString(), username, cookie, fullName, profilePicUrl) |
||||
|
return accountDataSource.getAccount(uid.toString()) |
||||
|
} |
||||
|
|
||||
|
suspend fun deleteAccount(account: Account) = accountDataSource.deleteAccount(account) |
||||
|
|
||||
|
suspend fun deleteAllAccounts() = accountDataSource.deleteAllAccounts() |
||||
|
|
||||
|
companion object { |
||||
|
private lateinit var instance: AccountRepository |
||||
|
|
||||
|
@JvmStatic |
||||
|
fun getInstance(accountDataSource: AccountDataSource): AccountRepository { |
||||
|
if (!this::instance.isInitialized) { |
||||
|
instance = AccountRepository(accountDataSource) |
||||
|
} |
||||
|
return instance |
||||
|
} |
||||
|
} |
||||
|
} |
@ -1,37 +0,0 @@ |
|||||
package awais.instagrabber.repositories; |
|
||||
|
|
||||
import java.util.Map; |
|
||||
|
|
||||
import awais.instagrabber.repositories.responses.FriendshipChangeResponse; |
|
||||
import awais.instagrabber.repositories.responses.FriendshipRestrictResponse; |
|
||||
import retrofit2.Call; |
|
||||
import retrofit2.http.FieldMap; |
|
||||
import retrofit2.http.FormUrlEncoded; |
|
||||
import retrofit2.http.GET; |
|
||||
import retrofit2.http.POST; |
|
||||
import retrofit2.http.Path; |
|
||||
import retrofit2.http.QueryMap; |
|
||||
|
|
||||
public interface FriendshipRepository { |
|
||||
|
|
||||
@FormUrlEncoded |
|
||||
@POST("/api/v1/friendships/{action}/{id}/") |
|
||||
Call<FriendshipChangeResponse> change(@Path("action") String action, |
|
||||
@Path("id") long id, |
|
||||
@FieldMap Map<String, String> form); |
|
||||
|
|
||||
@FormUrlEncoded |
|
||||
@POST("/api/v1/restrict_action/{action}/") |
|
||||
Call<FriendshipRestrictResponse> toggleRestrict(@Path("action") String action, |
|
||||
@FieldMap Map<String, String> form); |
|
||||
|
|
||||
@GET("/api/v1/friendships/{userId}/{type}/") |
|
||||
Call<String> getList(@Path("userId") long userId, |
|
||||
@Path("type") String type, // following or followers |
|
||||
@QueryMap(encoded = true) Map<String, String> queryParams); |
|
||||
|
|
||||
@FormUrlEncoded |
|
||||
@POST("/api/v1/friendships/{action}/") |
|
||||
Call<FriendshipChangeResponse> changeMute(@Path("action") String action, |
|
||||
@FieldMap Map<String, String> form); |
|
||||
} |
|
@ -0,0 +1,36 @@ |
|||||
|
package awais.instagrabber.repositories |
||||
|
|
||||
|
import awais.instagrabber.repositories.responses.FriendshipChangeResponse |
||||
|
import awais.instagrabber.repositories.responses.FriendshipRestrictResponse |
||||
|
import retrofit2.http.* |
||||
|
|
||||
|
interface FriendshipRepository { |
||||
|
@FormUrlEncoded |
||||
|
@POST("/api/v1/friendships/{action}/{id}/") |
||||
|
suspend fun change( |
||||
|
@Path("action") action: String, |
||||
|
@Path("id") id: Long, |
||||
|
@FieldMap form: Map<String, String>, |
||||
|
): FriendshipChangeResponse |
||||
|
|
||||
|
@FormUrlEncoded |
||||
|
@POST("/api/v1/restrict_action/{action}/") |
||||
|
suspend fun toggleRestrict( |
||||
|
@Path("action") action: String, |
||||
|
@FieldMap form: Map<String, String>, |
||||
|
): FriendshipRestrictResponse |
||||
|
|
||||
|
@GET("/api/v1/friendships/{userId}/{type}/") |
||||
|
suspend fun getList( |
||||
|
@Path("userId") userId: Long, |
||||
|
@Path("type") type: String, // following or followers |
||||
|
@QueryMap(encoded = true) queryParams: Map<String, String>, |
||||
|
): String |
||||
|
|
||||
|
@FormUrlEncoded |
||||
|
@POST("/api/v1/friendships/{action}/") |
||||
|
suspend fun changeMute( |
||||
|
@Path("action") action: String, |
||||
|
@FieldMap form: Map<String, String>, |
||||
|
): FriendshipChangeResponse |
||||
|
} |
@ -1,25 +0,0 @@ |
|||||
package awais.instagrabber.repositories; |
|
||||
|
|
||||
import java.util.Map; |
|
||||
|
|
||||
import retrofit2.Call; |
|
||||
import retrofit2.http.GET; |
|
||||
import retrofit2.http.Path; |
|
||||
import retrofit2.http.QueryMap; |
|
||||
|
|
||||
public interface GraphQLRepository { |
|
||||
@GET("/graphql/query/") |
|
||||
Call<String> fetch(@QueryMap(encoded = true) Map<String, String> queryParams); |
|
||||
|
|
||||
@GET("/{username}/?__a=1") |
|
||||
Call<String> getUser(@Path("username") String username); |
|
||||
|
|
||||
@GET("/p/{shortcode}/?__a=1") |
|
||||
Call<String> getPost(@Path("shortcode") String shortcode); |
|
||||
|
|
||||
@GET("/explore/tags/{tag}/?__a=1") |
|
||||
Call<String> getTag(@Path("tag") String tag); |
|
||||
|
|
||||
@GET("/explore/locations/{locationId}/?__a=1") |
|
||||
Call<String> getLocation(@Path("locationId") long locationId); |
|
||||
} |
|
@ -0,0 +1,22 @@ |
|||||
|
package awais.instagrabber.repositories |
||||
|
|
||||
|
import retrofit2.http.GET |
||||
|
import retrofit2.http.Path |
||||
|
import retrofit2.http.QueryMap |
||||
|
|
||||
|
interface GraphQLRepository { |
||||
|
@GET("/graphql/query/") |
||||
|
suspend fun fetch(@QueryMap(encoded = true) queryParams: Map<String, String>): String |
||||
|
|
||||
|
@GET("/{username}/?__a=1") |
||||
|
suspend fun getUser(@Path("username") username: String): String |
||||
|
|
||||
|
@GET("/p/{shortcode}/?__a=1") |
||||
|
suspend fun getPost(@Path("shortcode") shortcode: String): String |
||||
|
|
||||
|
@GET("/explore/tags/{tag}/?__a=1") |
||||
|
suspend fun getTag(@Path("tag") tag: String): String |
||||
|
|
||||
|
@GET("/explore/locations/{locationId}/?__a=1") |
||||
|
suspend fun getLocation(@Path("locationId") locationId: Long): String |
||||
|
} |
@ -1,43 +0,0 @@ |
|||||
package awais.instagrabber.repositories; |
|
||||
|
|
||||
import java.util.Map; |
|
||||
|
|
||||
import awais.instagrabber.repositories.responses.StoryStickerResponse; |
|
||||
import retrofit2.Call; |
|
||||
import retrofit2.http.FieldMap; |
|
||||
import retrofit2.http.FormUrlEncoded; |
|
||||
import retrofit2.http.GET; |
|
||||
import retrofit2.http.POST; |
|
||||
import retrofit2.http.Path; |
|
||||
import retrofit2.http.QueryMap; |
|
||||
import retrofit2.http.Url; |
|
||||
|
|
||||
public interface StoriesRepository { |
|
||||
@GET("/api/v1/media/{mediaId}/info/") |
|
||||
Call<String> fetch(@Path("mediaId") final long mediaId); |
|
||||
// this one is the same as MediaRepository.fetch BUT you need to make sure it's a story |
|
||||
|
|
||||
@GET("/api/v1/feed/reels_tray/") |
|
||||
Call<String> getFeedStories(); |
|
||||
|
|
||||
@GET("/api/v1/highlights/{uid}/highlights_tray/") |
|
||||
Call<String> fetchHighlights(@Path("uid") final long uid); |
|
||||
|
|
||||
@GET("/api/v1/archive/reel/day_shells/") |
|
||||
Call<String> fetchArchive(@QueryMap Map<String, String> queryParams); |
|
||||
|
|
||||
@GET |
|
||||
Call<String> getUserStory(@Url String url); |
|
||||
|
|
||||
@FormUrlEncoded |
|
||||
@POST("/api/v1/media/{storyId}/{stickerId}/{action}/") |
|
||||
Call<StoryStickerResponse> respondToSticker(@Path("storyId") String storyId, |
|
||||
@Path("stickerId") String stickerId, |
|
||||
@Path("action") String action, |
|
||||
// story_poll_vote, story_question_response, story_slider_vote, story_quiz_answer |
|
||||
@FieldMap Map<String, String> form); |
|
||||
|
|
||||
@FormUrlEncoded |
|
||||
@POST("/api/v2/media/seen/") |
|
||||
Call<String> seen(@QueryMap Map<String, String> queryParams, @FieldMap Map<String, String> form); |
|
||||
} |
|
@ -0,0 +1,38 @@ |
|||||
|
package awais.instagrabber.repositories |
||||
|
|
||||
|
import awais.instagrabber.repositories.responses.StoryStickerResponse |
||||
|
import retrofit2.http.* |
||||
|
|
||||
|
interface StoriesRepository { |
||||
|
// this one is the same as MediaRepository.fetch BUT you need to make sure it's a story |
||||
|
@GET("/api/v1/media/{mediaId}/info/") |
||||
|
suspend fun fetch(@Path("mediaId") mediaId: Long): String |
||||
|
|
||||
|
@GET("/api/v1/feed/reels_tray/") |
||||
|
suspend fun getFeedStories(): String |
||||
|
|
||||
|
@GET("/api/v1/highlights/{uid}/highlights_tray/") |
||||
|
suspend fun fetchHighlights(@Path("uid") uid: Long): String |
||||
|
|
||||
|
@GET("/api/v1/archive/reel/day_shells/") |
||||
|
suspend fun fetchArchive(@QueryMap queryParams: Map<String, String>): String |
||||
|
|
||||
|
@GET |
||||
|
suspend fun getUserStory(@Url url: String): String |
||||
|
|
||||
|
@FormUrlEncoded |
||||
|
@POST("/api/v1/media/{storyId}/{stickerId}/{action}/") |
||||
|
suspend fun respondToSticker( |
||||
|
@Path("storyId") storyId: String, |
||||
|
@Path("stickerId") stickerId: String, |
||||
|
@Path("action") action: String, // story_poll_vote, story_question_response, story_slider_vote, story_quiz_answer |
||||
|
@FieldMap form: Map<String, String>, |
||||
|
): StoryStickerResponse |
||||
|
|
||||
|
@FormUrlEncoded |
||||
|
@POST("/api/v2/media/seen/") |
||||
|
suspend fun seen( |
||||
|
@QueryMap queryParams: Map<String, String>, |
||||
|
@FieldMap form: Map<String, String>, |
||||
|
): String |
||||
|
} |
@ -1,25 +0,0 @@ |
|||||
package awais.instagrabber.repositories; |
|
||||
|
|
||||
import awais.instagrabber.repositories.responses.FriendshipStatus; |
|
||||
import awais.instagrabber.repositories.responses.UserSearchResponse; |
|
||||
import awais.instagrabber.repositories.responses.WrappedUser; |
|
||||
import retrofit2.Call; |
|
||||
import retrofit2.http.GET; |
|
||||
import retrofit2.http.Path; |
|
||||
import retrofit2.http.Query; |
|
||||
|
|
||||
public interface UserRepository { |
|
||||
|
|
||||
@GET("/api/v1/users/{uid}/info/") |
|
||||
Call<WrappedUser> getUserInfo(@Path("uid") final long uid); |
|
||||
|
|
||||
@GET("/api/v1/users/{username}/usernameinfo/") |
|
||||
Call<WrappedUser> getUsernameInfo(@Path("username") final String username); |
|
||||
|
|
||||
@GET("/api/v1/friendships/show/{uid}/") |
|
||||
Call<FriendshipStatus> getUserFriendship(@Path("uid") final long uid); |
|
||||
|
|
||||
@GET("/api/v1/users/search/") |
|
||||
Call<UserSearchResponse> search(@Query("timezone_offset") float timezoneOffset, |
|
||||
@Query("q") String query); |
|
||||
} |
|
@ -0,0 +1,25 @@ |
|||||
|
package awais.instagrabber.repositories |
||||
|
|
||||
|
import awais.instagrabber.repositories.responses.FriendshipStatus |
||||
|
import awais.instagrabber.repositories.responses.UserSearchResponse |
||||
|
import awais.instagrabber.repositories.responses.WrappedUser |
||||
|
import retrofit2.http.GET |
||||
|
import retrofit2.http.Path |
||||
|
import retrofit2.http.Query |
||||
|
|
||||
|
interface UserRepository { |
||||
|
@GET("/api/v1/users/{uid}/info/") |
||||
|
suspend fun getUserInfo(@Path("uid") uid: Long): WrappedUser |
||||
|
|
||||
|
@GET("/api/v1/users/{username}/usernameinfo/") |
||||
|
suspend fun getUsernameInfo(@Path("username") username: String): WrappedUser |
||||
|
|
||||
|
@GET("/api/v1/friendships/show/{uid}/") |
||||
|
suspend fun getUserFriendship(@Path("uid") uid: Long): FriendshipStatus |
||||
|
|
||||
|
@GET("/api/v1/users/search/") |
||||
|
suspend fun search( |
||||
|
@Query("timezone_offset") timezoneOffset: Float, |
||||
|
@Query("q") query: String, |
||||
|
): UserSearchResponse |
||||
|
} |
@ -1,296 +0,0 @@ |
|||||
package awais.instagrabber.repositories.responses; |
|
||||
|
|
||||
import java.io.Serializable; |
|
||||
import java.util.List; |
|
||||
import java.util.Objects; |
|
||||
|
|
||||
public class User implements Serializable { |
|
||||
private final long pk; |
|
||||
private final String username; |
|
||||
private final String fullName; |
|
||||
private final boolean isPrivate; |
|
||||
private final String profilePicUrl; |
|
||||
private final String profilePicId; |
|
||||
private FriendshipStatus friendshipStatus; |
|
||||
private final boolean isVerified; |
|
||||
private final boolean hasAnonymousProfilePicture; |
|
||||
private final boolean isUnpublished; |
|
||||
private final boolean isFavorite; |
|
||||
private final boolean isDirectappInstalled; |
|
||||
private final boolean hasChaining; |
|
||||
private final String reelAutoArchive; |
|
||||
private final String allowedCommenterType; |
|
||||
private final long mediaCount; |
|
||||
private final long followerCount; |
|
||||
private final long followingCount; |
|
||||
private final long followingTagCount; |
|
||||
private final String biography; |
|
||||
private final String externalUrl; |
|
||||
private final long usertagsCount; |
|
||||
private final String publicEmail; |
|
||||
private final HdProfilePicUrlInfo hdProfilePicUrlInfo; |
|
||||
private final String profileContext; // "also followed by" your friends |
|
||||
private final List<UserProfileContextLink> profileContextLinksWithUserIds; // ^ |
|
||||
private final String socialContext; // AYML |
|
||||
private final String interopMessagingUserFbid; // in DMs only: Facebook user ID |
|
||||
|
|
||||
public User(final long pk, |
|
||||
final String username, |
|
||||
final String fullName, |
|
||||
final boolean isPrivate, |
|
||||
final String profilePicUrl, |
|
||||
final String profilePicId, |
|
||||
final FriendshipStatus friendshipStatus, |
|
||||
final boolean isVerified, |
|
||||
final boolean hasAnonymousProfilePicture, |
|
||||
final boolean isUnpublished, |
|
||||
final boolean isFavorite, |
|
||||
final boolean isDirectappInstalled, |
|
||||
final boolean hasChaining, |
|
||||
final String reelAutoArchive, |
|
||||
final String allowedCommenterType, |
|
||||
final long mediaCount, |
|
||||
final long followerCount, |
|
||||
final long followingCount, |
|
||||
final long followingTagCount, |
|
||||
final String biography, |
|
||||
final String externalUrl, |
|
||||
final long usertagsCount, |
|
||||
final String publicEmail, |
|
||||
final HdProfilePicUrlInfo hdProfilePicUrlInfo, |
|
||||
final String profileContext, |
|
||||
final List<UserProfileContextLink> profileContextLinksWithUserIds, |
|
||||
final String socialContext, |
|
||||
final String interopMessagingUserFbid) { |
|
||||
this.pk = pk; |
|
||||
this.username = username; |
|
||||
this.fullName = fullName; |
|
||||
this.isPrivate = isPrivate; |
|
||||
this.profilePicUrl = profilePicUrl; |
|
||||
this.profilePicId = profilePicId; |
|
||||
this.friendshipStatus = friendshipStatus; |
|
||||
this.isVerified = isVerified; |
|
||||
this.hasAnonymousProfilePicture = hasAnonymousProfilePicture; |
|
||||
this.isUnpublished = isUnpublished; |
|
||||
this.isFavorite = isFavorite; |
|
||||
this.isDirectappInstalled = isDirectappInstalled; |
|
||||
this.hasChaining = hasChaining; |
|
||||
this.reelAutoArchive = reelAutoArchive; |
|
||||
this.allowedCommenterType = allowedCommenterType; |
|
||||
this.mediaCount = mediaCount; |
|
||||
this.followerCount = followerCount; |
|
||||
this.followingCount = followingCount; |
|
||||
this.followingTagCount = followingTagCount; |
|
||||
this.biography = biography; |
|
||||
this.externalUrl = externalUrl; |
|
||||
this.usertagsCount = usertagsCount; |
|
||||
this.publicEmail = publicEmail; |
|
||||
this.hdProfilePicUrlInfo = hdProfilePicUrlInfo; |
|
||||
this.profileContext = profileContext; |
|
||||
this.profileContextLinksWithUserIds = profileContextLinksWithUserIds; |
|
||||
this.socialContext = socialContext; |
|
||||
this.interopMessagingUserFbid = interopMessagingUserFbid; |
|
||||
} |
|
||||
|
|
||||
public User(final long pk, |
|
||||
final String username, |
|
||||
final String fullName, |
|
||||
final boolean isPrivate, |
|
||||
final String profilePicUrl, |
|
||||
final boolean isVerified) { |
|
||||
this.pk = pk; |
|
||||
this.username = username; |
|
||||
this.fullName = fullName; |
|
||||
this.isPrivate = isPrivate; |
|
||||
this.profilePicUrl = profilePicUrl; |
|
||||
this.profilePicId = null; |
|
||||
this.friendshipStatus = new FriendshipStatus( |
|
||||
false, |
|
||||
false, |
|
||||
false, |
|
||||
false, |
|
||||
false, |
|
||||
false, |
|
||||
false, |
|
||||
false, |
|
||||
false, |
|
||||
false |
|
||||
); |
|
||||
this.isVerified = isVerified; |
|
||||
this.hasAnonymousProfilePicture = false; |
|
||||
this.isUnpublished = false; |
|
||||
this.isFavorite = false; |
|
||||
this.isDirectappInstalled = false; |
|
||||
this.hasChaining = false; |
|
||||
this.reelAutoArchive = null; |
|
||||
this.allowedCommenterType = null; |
|
||||
this.mediaCount = 0; |
|
||||
this.followerCount = 0; |
|
||||
this.followingCount = 0; |
|
||||
this.followingTagCount = 0; |
|
||||
this.biography = null; |
|
||||
this.externalUrl = null; |
|
||||
this.usertagsCount = 0; |
|
||||
this.publicEmail = null; |
|
||||
this.hdProfilePicUrlInfo = null; |
|
||||
this.profileContext = null; |
|
||||
this.profileContextLinksWithUserIds = null; |
|
||||
this.socialContext = null; |
|
||||
this.interopMessagingUserFbid = null; |
|
||||
} |
|
||||
|
|
||||
public long getPk() { |
|
||||
return pk; |
|
||||
} |
|
||||
|
|
||||
public String getUsername() { |
|
||||
return username; |
|
||||
} |
|
||||
|
|
||||
public String getFullName() { |
|
||||
return fullName; |
|
||||
} |
|
||||
|
|
||||
public boolean isPrivate() { |
|
||||
return isPrivate; |
|
||||
} |
|
||||
|
|
||||
public String getProfilePicUrl() { |
|
||||
return profilePicUrl; |
|
||||
} |
|
||||
|
|
||||
public String getHDProfilePicUrl() { |
|
||||
if (hdProfilePicUrlInfo == null) { |
|
||||
return getProfilePicUrl(); |
|
||||
} |
|
||||
return hdProfilePicUrlInfo.getUrl(); |
|
||||
} |
|
||||
|
|
||||
public String getProfilePicId() { |
|
||||
return profilePicId; |
|
||||
} |
|
||||
|
|
||||
public FriendshipStatus getFriendshipStatus() { |
|
||||
return friendshipStatus; |
|
||||
} |
|
||||
|
|
||||
public void setFriendshipStatus(final FriendshipStatus friendshipStatus) { |
|
||||
this.friendshipStatus = friendshipStatus; |
|
||||
} |
|
||||
|
|
||||
public boolean isVerified() { |
|
||||
return isVerified; |
|
||||
} |
|
||||
|
|
||||
public boolean hasAnonymousProfilePicture() { |
|
||||
return hasAnonymousProfilePicture; |
|
||||
} |
|
||||
|
|
||||
public boolean isUnpublished() { |
|
||||
return isUnpublished; |
|
||||
} |
|
||||
|
|
||||
public boolean isFavorite() { |
|
||||
return isFavorite; |
|
||||
} |
|
||||
|
|
||||
public boolean isDirectappInstalled() { |
|
||||
return isDirectappInstalled; |
|
||||
} |
|
||||
|
|
||||
public boolean hasChaining() { |
|
||||
return hasChaining; |
|
||||
} |
|
||||
|
|
||||
public String getReelAutoArchive() { |
|
||||
return reelAutoArchive; |
|
||||
} |
|
||||
|
|
||||
public String getAllowedCommenterType() { |
|
||||
return allowedCommenterType; |
|
||||
} |
|
||||
|
|
||||
public long getMediaCount() { |
|
||||
return mediaCount; |
|
||||
} |
|
||||
|
|
||||
public long getFollowerCount() { |
|
||||
return followerCount; |
|
||||
} |
|
||||
|
|
||||
public long getFollowingCount() { |
|
||||
return followingCount; |
|
||||
} |
|
||||
|
|
||||
public long getFollowingTagCount() { |
|
||||
return followingTagCount; |
|
||||
} |
|
||||
|
|
||||
public String getBiography() { |
|
||||
return biography; |
|
||||
} |
|
||||
|
|
||||
public String getExternalUrl() { |
|
||||
return externalUrl; |
|
||||
} |
|
||||
|
|
||||
public long getUsertagsCount() { |
|
||||
return usertagsCount; |
|
||||
} |
|
||||
|
|
||||
public String getPublicEmail() { |
|
||||
return publicEmail; |
|
||||
} |
|
||||
|
|
||||
public String getProfileContext() { |
|
||||
return profileContext; |
|
||||
} |
|
||||
|
|
||||
public String getSocialContext() { |
|
||||
return socialContext; |
|
||||
} |
|
||||
|
|
||||
public List<UserProfileContextLink> getProfileContextLinks() { |
|
||||
return profileContextLinksWithUserIds; |
|
||||
} |
|
||||
|
|
||||
public String getFbId() { |
|
||||
return interopMessagingUserFbid; |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public boolean equals(final Object o) { |
|
||||
if (this == o) return true; |
|
||||
if (o == null || getClass() != o.getClass()) return false; |
|
||||
final User user = (User) o; |
|
||||
return pk == user.pk && |
|
||||
isPrivate == user.isPrivate && |
|
||||
isVerified == user.isVerified && |
|
||||
hasAnonymousProfilePicture == user.hasAnonymousProfilePicture && |
|
||||
isUnpublished == user.isUnpublished && |
|
||||
isFavorite == user.isFavorite && |
|
||||
isDirectappInstalled == user.isDirectappInstalled && |
|
||||
mediaCount == user.mediaCount && |
|
||||
followerCount == user.followerCount && |
|
||||
followingCount == user.followingCount && |
|
||||
followingTagCount == user.followingTagCount && |
|
||||
usertagsCount == user.usertagsCount && |
|
||||
Objects.equals(username, user.username) && |
|
||||
Objects.equals(fullName, user.fullName) && |
|
||||
Objects.equals(profilePicUrl, user.profilePicUrl) && |
|
||||
Objects.equals(profilePicId, user.profilePicId) && |
|
||||
Objects.equals(friendshipStatus, user.friendshipStatus) && |
|
||||
Objects.equals(reelAutoArchive, user.reelAutoArchive) && |
|
||||
Objects.equals(allowedCommenterType, user.allowedCommenterType) && |
|
||||
Objects.equals(biography, user.biography) && |
|
||||
Objects.equals(externalUrl, user.externalUrl) && |
|
||||
Objects.equals(publicEmail, user.publicEmail); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public int hashCode() { |
|
||||
return Objects.hash(pk, username, fullName, isPrivate, profilePicUrl, profilePicId, friendshipStatus, isVerified, hasAnonymousProfilePicture, |
|
||||
isUnpublished, isFavorite, isDirectappInstalled, hasChaining, reelAutoArchive, allowedCommenterType, mediaCount, |
|
||||
followerCount, followingCount, followingTagCount, biography, externalUrl, usertagsCount, publicEmail); |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,38 @@ |
|||||
|
package awais.instagrabber.repositories.responses |
||||
|
|
||||
|
import java.io.Serializable |
||||
|
|
||||
|
|
||||
|
data class User @JvmOverloads constructor( |
||||
|
val pk: Long = 0, |
||||
|
val username: String = "", |
||||
|
val fullName: String = "", |
||||
|
val isPrivate: Boolean = false, |
||||
|
val profilePicUrl: String? = null, |
||||
|
val isVerified: Boolean = false, |
||||
|
val profilePicId: String? = null, |
||||
|
var friendshipStatus: FriendshipStatus? = null, |
||||
|
val hasAnonymousProfilePicture: Boolean = false, |
||||
|
val isUnpublished: Boolean = false, |
||||
|
val isFavorite: Boolean = false, |
||||
|
val isDirectappInstalled: Boolean = false, |
||||
|
val hasChaining: Boolean = false, |
||||
|
val reelAutoArchive: String? = null, |
||||
|
val allowedCommenterType: String? = null, |
||||
|
val mediaCount: Long = 0, |
||||
|
val followerCount: Long = 0, |
||||
|
val followingCount: Long = 0, |
||||
|
val followingTagCount: Long = 0, |
||||
|
val biography: String? = null, |
||||
|
val externalUrl: String? = null, |
||||
|
val usertagsCount: Long = 0, |
||||
|
val publicEmail: String? = null, |
||||
|
val hdProfilePicUrlInfo: HdProfilePicUrlInfo? = null, |
||||
|
val profileContext: String? = null, // "also followed by" your friends |
||||
|
val profileContextLinksWithUserIds: List<UserProfileContextLink>? = null, // ^ |
||||
|
val socialContext: String? = null, // AYML |
||||
|
val interopMessagingUserFbid: String? = null, // in DMs only: Facebook user ID |
||||
|
) : Serializable { |
||||
|
val hDProfilePicUrl: String |
||||
|
get() = hdProfilePicUrlInfo?.url ?: profilePicUrl ?: "" |
||||
|
} |
@ -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()) |
||||
|
} |
||||
|
} |
@ -1,264 +0,0 @@ |
|||||
package awais.instagrabber.webservices; |
|
||||
|
|
||||
import android.util.Log; |
|
||||
|
|
||||
import androidx.annotation.NonNull; |
|
||||
|
|
||||
import org.json.JSONArray; |
|
||||
import org.json.JSONException; |
|
||||
import org.json.JSONObject; |
|
||||
|
|
||||
import java.util.ArrayList; |
|
||||
import java.util.Collections; |
|
||||
import java.util.HashMap; |
|
||||
import java.util.List; |
|
||||
import java.util.Map; |
|
||||
import java.util.Objects; |
|
||||
|
|
||||
import awais.instagrabber.models.FollowModel; |
|
||||
import awais.instagrabber.repositories.FriendshipRepository; |
|
||||
import awais.instagrabber.repositories.responses.FriendshipChangeResponse; |
|
||||
import awais.instagrabber.repositories.responses.FriendshipListFetchResponse; |
|
||||
import awais.instagrabber.repositories.responses.FriendshipRestrictResponse; |
|
||||
import awais.instagrabber.utils.TextUtils; |
|
||||
import awais.instagrabber.utils.Utils; |
|
||||
import retrofit2.Call; |
|
||||
import retrofit2.Callback; |
|
||||
import retrofit2.Response; |
|
||||
|
|
||||
public class FriendshipService extends BaseService { |
|
||||
private static final String TAG = "FriendshipService"; |
|
||||
|
|
||||
private final FriendshipRepository repository; |
|
||||
private final String deviceUuid, csrfToken; |
|
||||
private final long userId; |
|
||||
|
|
||||
private static FriendshipService instance; |
|
||||
|
|
||||
private FriendshipService(final String deviceUuid, |
|
||||
final String csrfToken, |
|
||||
final long userId) { |
|
||||
this.deviceUuid = deviceUuid; |
|
||||
this.csrfToken = csrfToken; |
|
||||
this.userId = userId; |
|
||||
repository = RetrofitFactory.INSTANCE |
|
||||
.getRetrofit() |
|
||||
.create(FriendshipRepository.class); |
|
||||
} |
|
||||
|
|
||||
public String getCsrfToken() { |
|
||||
return csrfToken; |
|
||||
} |
|
||||
|
|
||||
public String getDeviceUuid() { |
|
||||
return deviceUuid; |
|
||||
} |
|
||||
|
|
||||
public long getUserId() { |
|
||||
return userId; |
|
||||
} |
|
||||
|
|
||||
public static FriendshipService getInstance(final String deviceUuid, final String csrfToken, final long userId) { |
|
||||
if (instance == null |
|
||||
|| !Objects.equals(instance.getCsrfToken(), csrfToken) |
|
||||
|| !Objects.equals(instance.getDeviceUuid(), deviceUuid) |
|
||||
|| !Objects.equals(instance.getUserId(), userId)) { |
|
||||
instance = new FriendshipService(deviceUuid, csrfToken, userId); |
|
||||
} |
|
||||
return instance; |
|
||||
} |
|
||||
|
|
||||
public void follow(final long targetUserId, |
|
||||
final ServiceCallback<FriendshipChangeResponse> callback) { |
|
||||
change("create", targetUserId, callback); |
|
||||
} |
|
||||
|
|
||||
public void unfollow(final long targetUserId, |
|
||||
final ServiceCallback<FriendshipChangeResponse> callback) { |
|
||||
change("destroy", targetUserId, callback); |
|
||||
} |
|
||||
|
|
||||
public void changeBlock(final boolean unblock, |
|
||||
final long targetUserId, |
|
||||
final ServiceCallback<FriendshipChangeResponse> callback) { |
|
||||
change(unblock ? "unblock" : "block", targetUserId, callback); |
|
||||
} |
|
||||
|
|
||||
public void toggleRestrict(final long targetUserId, |
|
||||
final boolean restrict, |
|
||||
final ServiceCallback<FriendshipRestrictResponse> callback) { |
|
||||
final Map<String, String> form = new HashMap<>(3); |
|
||||
form.put("_csrftoken", csrfToken); |
|
||||
form.put("_uuid", deviceUuid); |
|
||||
form.put("target_user_id", String.valueOf(targetUserId)); |
|
||||
final String action = restrict ? "restrict" : "unrestrict"; |
|
||||
final Call<FriendshipRestrictResponse> request = repository.toggleRestrict(action, form); |
|
||||
request.enqueue(new Callback<FriendshipRestrictResponse>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<FriendshipRestrictResponse> call, |
|
||||
@NonNull final Response<FriendshipRestrictResponse> response) { |
|
||||
if (callback != null) { |
|
||||
callback.onSuccess(response.body()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<FriendshipRestrictResponse> call, |
|
||||
@NonNull final Throwable t) { |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public void approve(final long targetUserId, |
|
||||
final ServiceCallback<FriendshipChangeResponse> callback) { |
|
||||
change("approve", targetUserId, callback); |
|
||||
} |
|
||||
|
|
||||
public void ignore(final long targetUserId, |
|
||||
final ServiceCallback<FriendshipChangeResponse> callback) { |
|
||||
change("ignore", targetUserId, callback); |
|
||||
} |
|
||||
|
|
||||
public void removeFollower(final long targetUserId, |
|
||||
final ServiceCallback<FriendshipChangeResponse> callback) { |
|
||||
change("remove_follower", targetUserId, callback); |
|
||||
} |
|
||||
|
|
||||
private void change(final String action, |
|
||||
final long targetUserId, |
|
||||
final ServiceCallback<FriendshipChangeResponse> callback) { |
|
||||
final Map<String, Object> form = new HashMap<>(5); |
|
||||
form.put("_csrftoken", csrfToken); |
|
||||
form.put("_uid", userId); |
|
||||
form.put("_uuid", deviceUuid); |
|
||||
form.put("radio_type", "wifi-none"); |
|
||||
form.put("user_id", targetUserId); |
|
||||
final Map<String, String> signedForm = Utils.sign(form); |
|
||||
final Call<FriendshipChangeResponse> request = repository.change(action, targetUserId, signedForm); |
|
||||
request.enqueue(new Callback<FriendshipChangeResponse>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<FriendshipChangeResponse> call, |
|
||||
@NonNull final Response<FriendshipChangeResponse> response) { |
|
||||
if (callback != null) { |
|
||||
callback.onSuccess(response.body()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<FriendshipChangeResponse> call, |
|
||||
@NonNull final Throwable t) { |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public void changeMute(final boolean unmute, |
|
||||
final long targetUserId, |
|
||||
final boolean story, // true for story, false for posts |
|
||||
final ServiceCallback<FriendshipChangeResponse> callback) { |
|
||||
final Map<String, String> form = new HashMap<>(4); |
|
||||
form.put("_csrftoken", csrfToken); |
|
||||
form.put("_uid", String.valueOf(userId)); |
|
||||
form.put("_uuid", deviceUuid); |
|
||||
form.put(story ? "target_reel_author_id" : "target_posts_author_id", String.valueOf(targetUserId)); |
|
||||
final Call<FriendshipChangeResponse> request = repository.changeMute(unmute ? |
|
||||
"unmute_posts_or_story_from_follow" : |
|
||||
"mute_posts_or_story_from_follow", |
|
||||
form); |
|
||||
request.enqueue(new Callback<FriendshipChangeResponse>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<FriendshipChangeResponse> call, |
|
||||
@NonNull final Response<FriendshipChangeResponse> response) { |
|
||||
if (callback != null) { |
|
||||
callback.onSuccess(response.body()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<FriendshipChangeResponse> call, |
|
||||
@NonNull final Throwable t) { |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public void getList(final boolean follower, |
|
||||
final long targetUserId, |
|
||||
final String maxId, |
|
||||
final ServiceCallback<FriendshipListFetchResponse> callback) { |
|
||||
final Map<String, String> queryMap = new HashMap<>(); |
|
||||
if (maxId != null) queryMap.put("max_id", maxId); |
|
||||
final Call<String> request = repository.getList( |
|
||||
targetUserId, |
|
||||
follower ? "followers" : "following", |
|
||||
queryMap); |
|
||||
request.enqueue(new Callback<String>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { |
|
||||
try { |
|
||||
if (callback == null) { |
|
||||
return; |
|
||||
} |
|
||||
final String body = response.body(); |
|
||||
if (TextUtils.isEmpty(body)) { |
|
||||
callback.onSuccess(null); |
|
||||
return; |
|
||||
} |
|
||||
final FriendshipListFetchResponse friendshipListFetchResponse = parseListResponse(body); |
|
||||
callback.onSuccess(friendshipListFetchResponse); |
|
||||
} catch (JSONException e) { |
|
||||
Log.e(TAG, "onResponse", e); |
|
||||
callback.onFailure(e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private FriendshipListFetchResponse parseListResponse(@NonNull final String body) throws JSONException { |
|
||||
final JSONObject root = new JSONObject(body); |
|
||||
final String nextMaxId = root.optString("next_max_id"); |
|
||||
final String status = root.optString("status"); |
|
||||
final JSONArray itemsJson = root.optJSONArray("users"); |
|
||||
final List<FollowModel> items = parseItems(itemsJson); |
|
||||
return new FriendshipListFetchResponse( |
|
||||
nextMaxId, |
|
||||
status, |
|
||||
items |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
private List<FollowModel> parseItems(final JSONArray items) throws JSONException { |
|
||||
if (items == null) { |
|
||||
return Collections.emptyList(); |
|
||||
} |
|
||||
final List<FollowModel> followModels = new ArrayList<>(); |
|
||||
for (int i = 0; i < items.length(); i++) { |
|
||||
final JSONObject itemJson = items.optJSONObject(i); |
|
||||
if (itemJson == null) { |
|
||||
continue; |
|
||||
} |
|
||||
final FollowModel followModel = new FollowModel(itemJson.getString("pk"), |
|
||||
itemJson.getString("username"), |
|
||||
itemJson.optString("full_name"), |
|
||||
itemJson.getString("profile_pic_url")); |
|
||||
if (followModel != null) { |
|
||||
followModels.add(followModel); |
|
||||
} |
|
||||
} |
|
||||
return followModels; |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,155 @@ |
|||||
|
package awais.instagrabber.webservices |
||||
|
|
||||
|
import awais.instagrabber.models.FollowModel |
||||
|
import awais.instagrabber.repositories.FriendshipRepository |
||||
|
import awais.instagrabber.repositories.responses.FriendshipChangeResponse |
||||
|
import awais.instagrabber.repositories.responses.FriendshipListFetchResponse |
||||
|
import awais.instagrabber.repositories.responses.FriendshipRestrictResponse |
||||
|
import awais.instagrabber.utils.Utils |
||||
|
import awais.instagrabber.webservices.RetrofitFactory.retrofit |
||||
|
import org.json.JSONArray |
||||
|
import org.json.JSONException |
||||
|
import org.json.JSONObject |
||||
|
|
||||
|
object FriendshipService : BaseService() { |
||||
|
private val repository: FriendshipRepository = retrofit.create(FriendshipRepository::class.java) |
||||
|
|
||||
|
suspend fun follow( |
||||
|
csrfToken: String, |
||||
|
userId: Long, |
||||
|
deviceUuid: String, |
||||
|
targetUserId: Long, |
||||
|
): FriendshipChangeResponse = change(csrfToken, userId, deviceUuid, "create", targetUserId) |
||||
|
|
||||
|
suspend fun unfollow( |
||||
|
csrfToken: String, |
||||
|
userId: Long, |
||||
|
deviceUuid: String, |
||||
|
targetUserId: Long, |
||||
|
): FriendshipChangeResponse = change(csrfToken, userId, deviceUuid, "destroy", targetUserId) |
||||
|
|
||||
|
suspend fun changeBlock( |
||||
|
csrfToken: String, |
||||
|
userId: Long, |
||||
|
deviceUuid: String, |
||||
|
unblock: Boolean, |
||||
|
targetUserId: Long, |
||||
|
): FriendshipChangeResponse { |
||||
|
return change(csrfToken, userId, deviceUuid, if (unblock) "unblock" else "block", targetUserId) |
||||
|
} |
||||
|
|
||||
|
suspend fun toggleRestrict( |
||||
|
csrfToken: String, |
||||
|
deviceUuid: String, |
||||
|
targetUserId: Long, |
||||
|
restrict: Boolean, |
||||
|
): FriendshipRestrictResponse { |
||||
|
val form = mapOf( |
||||
|
"_csrftoken" to csrfToken, |
||||
|
"_uuid" to deviceUuid, |
||||
|
"target_user_id" to targetUserId.toString(), |
||||
|
) |
||||
|
val action = if (restrict) "restrict" else "unrestrict" |
||||
|
return repository.toggleRestrict(action, form) |
||||
|
} |
||||
|
|
||||
|
suspend fun approve( |
||||
|
csrfToken: String, |
||||
|
userId: Long, |
||||
|
deviceUuid: String, |
||||
|
targetUserId: Long, |
||||
|
): FriendshipChangeResponse = change(csrfToken, userId, deviceUuid, "approve", targetUserId) |
||||
|
|
||||
|
suspend fun ignore( |
||||
|
csrfToken: String, |
||||
|
userId: Long, |
||||
|
deviceUuid: String, |
||||
|
targetUserId: Long, |
||||
|
): FriendshipChangeResponse = change(csrfToken, userId, deviceUuid, "ignore", targetUserId) |
||||
|
|
||||
|
suspend fun removeFollower( |
||||
|
csrfToken: String, |
||||
|
userId: Long, |
||||
|
deviceUuid: String, |
||||
|
targetUserId: Long, |
||||
|
): FriendshipChangeResponse = change(csrfToken, userId, deviceUuid, "remove_follower", targetUserId) |
||||
|
|
||||
|
private suspend fun change( |
||||
|
csrfToken: String, |
||||
|
userId: Long, |
||||
|
deviceUuid: String, |
||||
|
action: String, |
||||
|
targetUserId: Long, |
||||
|
): FriendshipChangeResponse { |
||||
|
val form = mapOf( |
||||
|
"_csrftoken" to csrfToken, |
||||
|
"_uid" to userId, |
||||
|
"_uuid" to deviceUuid, |
||||
|
"radio_type" to "wifi-none", |
||||
|
"user_id" to targetUserId, |
||||
|
) |
||||
|
val signedForm = Utils.sign(form) |
||||
|
return repository.change(action, targetUserId, signedForm) |
||||
|
} |
||||
|
|
||||
|
suspend fun changeMute( |
||||
|
csrfToken: String, |
||||
|
userId: Long, |
||||
|
deviceUuid: String, |
||||
|
unmute: Boolean, |
||||
|
targetUserId: Long, |
||||
|
story: Boolean, // true for story, false for posts |
||||
|
): FriendshipChangeResponse { |
||||
|
val form = mapOf( |
||||
|
"_csrftoken" to csrfToken, |
||||
|
"_uid" to userId.toString(), |
||||
|
"_uuid" to deviceUuid, |
||||
|
(if (story) "target_reel_author_id" else "target_posts_author_id") to targetUserId.toString(), |
||||
|
) |
||||
|
return repository.changeMute( |
||||
|
if (unmute) "unmute_posts_or_story_from_follow" else "mute_posts_or_story_from_follow", |
||||
|
form |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
suspend fun getList( |
||||
|
follower: Boolean, |
||||
|
targetUserId: Long, |
||||
|
maxId: String?, |
||||
|
): FriendshipListFetchResponse { |
||||
|
val queryMap = if (maxId != null) mapOf("max_id" to maxId) else emptyMap() |
||||
|
val response = repository.getList(targetUserId, if (follower) "followers" else "following", queryMap) |
||||
|
return parseListResponse(response) |
||||
|
} |
||||
|
|
||||
|
@Throws(JSONException::class) |
||||
|
private fun parseListResponse(body: String): FriendshipListFetchResponse { |
||||
|
val root = JSONObject(body) |
||||
|
val nextMaxId = root.optString("next_max_id") |
||||
|
val status = root.optString("status") |
||||
|
val itemsJson = root.optJSONArray("users") |
||||
|
val items = parseItems(itemsJson) |
||||
|
return FriendshipListFetchResponse( |
||||
|
nextMaxId, |
||||
|
status, |
||||
|
items |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
@Throws(JSONException::class) |
||||
|
private fun parseItems(items: JSONArray?): List<FollowModel> { |
||||
|
if (items == null) { |
||||
|
return emptyList() |
||||
|
} |
||||
|
val followModels = mutableListOf<FollowModel>() |
||||
|
for (i in 0 until items.length()) { |
||||
|
val itemJson = items.optJSONObject(i) ?: continue |
||||
|
val followModel = FollowModel(itemJson.getString("pk"), |
||||
|
itemJson.getString("username"), |
||||
|
itemJson.optString("full_name"), |
||||
|
itemJson.getString("profile_pic_url")) |
||||
|
followModels.add(followModel) |
||||
|
} |
||||
|
return followModels |
||||
|
} |
||||
|
} |
@ -1,483 +0,0 @@ |
|||||
package awais.instagrabber.webservices; |
|
||||
|
|
||||
import android.util.Log; |
|
||||
|
|
||||
import androidx.annotation.NonNull; |
|
||||
|
|
||||
import com.google.common.collect.ImmutableMap; |
|
||||
|
|
||||
import org.json.JSONArray; |
|
||||
import org.json.JSONException; |
|
||||
import org.json.JSONObject; |
|
||||
|
|
||||
import java.util.ArrayList; |
|
||||
import java.util.Collections; |
|
||||
import java.util.HashMap; |
|
||||
import java.util.List; |
|
||||
import java.util.Map; |
|
||||
|
|
||||
import awais.instagrabber.models.enums.FollowingType; |
|
||||
import awais.instagrabber.repositories.GraphQLRepository; |
|
||||
import awais.instagrabber.repositories.responses.FriendshipStatus; |
|
||||
import awais.instagrabber.repositories.responses.GraphQLUserListFetchResponse; |
|
||||
import awais.instagrabber.repositories.responses.Hashtag; |
|
||||
import awais.instagrabber.repositories.responses.Location; |
|
||||
import awais.instagrabber.repositories.responses.Media; |
|
||||
import awais.instagrabber.repositories.responses.PostsFetchResponse; |
|
||||
import awais.instagrabber.repositories.responses.User; |
|
||||
import awais.instagrabber.utils.Constants; |
|
||||
import awais.instagrabber.utils.ResponseBodyUtils; |
|
||||
import awais.instagrabber.utils.TextUtils; |
|
||||
import retrofit2.Call; |
|
||||
import retrofit2.Callback; |
|
||||
import retrofit2.Response; |
|
||||
|
|
||||
public class GraphQLService extends BaseService { |
|
||||
private static final String TAG = "GraphQLService"; |
|
||||
|
|
||||
private final GraphQLRepository repository; |
|
||||
|
|
||||
private static GraphQLService instance; |
|
||||
|
|
||||
private GraphQLService() { |
|
||||
repository = RetrofitFactory.INSTANCE |
|
||||
.getRetrofitWeb() |
|
||||
.create(GraphQLRepository.class); |
|
||||
} |
|
||||
|
|
||||
public static GraphQLService getInstance() { |
|
||||
if (instance == null) { |
|
||||
instance = new GraphQLService(); |
|
||||
} |
|
||||
return instance; |
|
||||
} |
|
||||
|
|
||||
// TODO convert string response to a response class |
|
||||
private void fetch(final String queryHash, |
|
||||
final String variables, |
|
||||
final String arg1, |
|
||||
final String arg2, |
|
||||
final User backup, |
|
||||
final ServiceCallback<PostsFetchResponse> callback) { |
|
||||
final Map<String, String> queryMap = new HashMap<>(); |
|
||||
queryMap.put("query_hash", queryHash); |
|
||||
queryMap.put("variables", variables); |
|
||||
final Call<String> request = repository.fetch(queryMap); |
|
||||
request.enqueue(new Callback<String>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { |
|
||||
try { |
|
||||
// Log.d(TAG, "onResponse: body: " + response.body()); |
|
||||
final PostsFetchResponse postsFetchResponse = parsePostResponse(response, arg1, arg2, backup); |
|
||||
if (callback != null) { |
|
||||
callback.onSuccess(postsFetchResponse); |
|
||||
} |
|
||||
} catch (JSONException e) { |
|
||||
Log.e(TAG, "onResponse", e); |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(e); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public void fetchLocationPosts(final long locationId, |
|
||||
final String maxId, |
|
||||
final ServiceCallback<PostsFetchResponse> callback) { |
|
||||
fetch("36bd0f2bf5911908de389b8ceaa3be6d", |
|
||||
"{\"id\":\"" + locationId + "\"," + |
|
||||
"\"first\":25," + |
|
||||
"\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", |
|
||||
Constants.EXTRAS_LOCATION, |
|
||||
"edge_location_to_media", |
|
||||
null, |
|
||||
callback); |
|
||||
} |
|
||||
|
|
||||
public void fetchHashtagPosts(@NonNull final String tag, |
|
||||
final String maxId, |
|
||||
final ServiceCallback<PostsFetchResponse> callback) { |
|
||||
fetch("9b498c08113f1e09617a1703c22b2f32", |
|
||||
"{\"tag_name\":\"" + tag + "\"," + |
|
||||
"\"first\":25," + |
|
||||
"\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", |
|
||||
Constants.EXTRAS_HASHTAG, |
|
||||
"edge_hashtag_to_media", |
|
||||
null, |
|
||||
callback); |
|
||||
} |
|
||||
|
|
||||
public void fetchProfilePosts(final long profileId, |
|
||||
final int postsPerPage, |
|
||||
final String maxId, |
|
||||
final User backup, |
|
||||
final ServiceCallback<PostsFetchResponse> callback) { |
|
||||
fetch("02e14f6a7812a876f7d133c9555b1151", |
|
||||
"{\"id\":\"" + profileId + "\"," + |
|
||||
"\"first\":" + postsPerPage + "," + |
|
||||
"\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", |
|
||||
Constants.EXTRAS_USER, |
|
||||
"edge_owner_to_timeline_media", |
|
||||
backup, |
|
||||
callback); |
|
||||
} |
|
||||
|
|
||||
public void fetchTaggedPosts(final long profileId, |
|
||||
final int postsPerPage, |
|
||||
final String maxId, |
|
||||
final ServiceCallback<PostsFetchResponse> callback) { |
|
||||
fetch("31fe64d9463cbbe58319dced405c6206", |
|
||||
"{\"id\":\"" + profileId + "\"," + |
|
||||
"\"first\":" + postsPerPage + "," + |
|
||||
"\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", |
|
||||
Constants.EXTRAS_USER, |
|
||||
"edge_user_to_photos_of_you", |
|
||||
null, |
|
||||
callback); |
|
||||
} |
|
||||
|
|
||||
@NonNull |
|
||||
private PostsFetchResponse parsePostResponse(@NonNull final Response<String> response, |
|
||||
@NonNull final String arg1, |
|
||||
@NonNull final String arg2, |
|
||||
final User backup) |
|
||||
throws JSONException { |
|
||||
if (TextUtils.isEmpty(response.body())) { |
|
||||
Log.e(TAG, "parseResponse: feed response body is empty with status code: " + response.code()); |
|
||||
return new PostsFetchResponse(Collections.emptyList(), false, null); |
|
||||
} |
|
||||
return parseResponseBody(response.body(), arg1, arg2, backup); |
|
||||
} |
|
||||
|
|
||||
@NonNull |
|
||||
private PostsFetchResponse parseResponseBody(@NonNull final String body, |
|
||||
@NonNull final String arg1, |
|
||||
@NonNull final String arg2, |
|
||||
final User backup) |
|
||||
throws JSONException { |
|
||||
final List<Media> items = new ArrayList<>(); |
|
||||
final JSONObject timelineFeed = new JSONObject(body) |
|
||||
.getJSONObject("data") |
|
||||
.getJSONObject(arg1) |
|
||||
.getJSONObject(arg2); |
|
||||
final String endCursor; |
|
||||
final boolean hasNextPage; |
|
||||
|
|
||||
final JSONObject pageInfo = timelineFeed.getJSONObject("page_info"); |
|
||||
if (pageInfo.has("has_next_page")) { |
|
||||
hasNextPage = pageInfo.getBoolean("has_next_page"); |
|
||||
endCursor = hasNextPage ? pageInfo.getString("end_cursor") : null; |
|
||||
} else { |
|
||||
hasNextPage = false; |
|
||||
endCursor = null; |
|
||||
} |
|
||||
|
|
||||
final JSONArray feedItems = timelineFeed.getJSONArray("edges"); |
|
||||
|
|
||||
for (int i = 0; i < feedItems.length(); ++i) { |
|
||||
final JSONObject itemJson = feedItems.optJSONObject(i); |
|
||||
if (itemJson == null) { |
|
||||
continue; |
|
||||
} |
|
||||
final Media media = ResponseBodyUtils.parseGraphQLItem(itemJson, backup); |
|
||||
if (media != null) { |
|
||||
items.add(media); |
|
||||
} |
|
||||
} |
|
||||
return new PostsFetchResponse(items, hasNextPage, endCursor); |
|
||||
} |
|
||||
|
|
||||
// TODO convert string response to a response class |
|
||||
public void fetchCommentLikers(final String commentId, |
|
||||
final String endCursor, |
|
||||
final ServiceCallback<GraphQLUserListFetchResponse> callback) { |
|
||||
final Map<String, String> queryMap = new HashMap<>(); |
|
||||
queryMap.put("query_hash", "5f0b1f6281e72053cbc07909c8d154ae"); |
|
||||
queryMap.put("variables", "{\"comment_id\":\"" + commentId + "\"," + |
|
||||
"\"first\":30," + |
|
||||
"\"after\":\"" + (endCursor == null ? "" : endCursor) + "\"}"); |
|
||||
final Call<String> request = repository.fetch(queryMap); |
|
||||
request.enqueue(new Callback<String>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { |
|
||||
final String rawBody = response.body(); |
|
||||
if (rawBody == null) { |
|
||||
Log.e(TAG, "Error occurred while fetching gql comment likes of " + commentId); |
|
||||
callback.onSuccess(null); |
|
||||
return; |
|
||||
} |
|
||||
try { |
|
||||
final JSONObject body = new JSONObject(rawBody); |
|
||||
final String status = body.getString("status"); |
|
||||
final JSONObject data = body.getJSONObject("data").getJSONObject("comment").getJSONObject("edge_liked_by"); |
|
||||
final JSONObject pageInfo = data.getJSONObject("page_info"); |
|
||||
final String endCursor = pageInfo.getBoolean("has_next_page") ? pageInfo.getString("end_cursor") : null; |
|
||||
final JSONArray users = data.getJSONArray("edges"); |
|
||||
final int usersLen = users.length(); |
|
||||
final List<User> userModels = new ArrayList<>(); |
|
||||
for (int j = 0; j < usersLen; ++j) { |
|
||||
final JSONObject userObject = users.getJSONObject(j).getJSONObject("node"); |
|
||||
userModels.add(new User( |
|
||||
userObject.getLong("id"), |
|
||||
userObject.getString("username"), |
|
||||
userObject.optString("full_name"), |
|
||||
userObject.optBoolean("is_private"), |
|
||||
userObject.getString("profile_pic_url"), |
|
||||
userObject.optBoolean("is_verified") |
|
||||
)); |
|
||||
// userModels.add(new ProfileModel(userObject.optBoolean("is_private"), |
|
||||
// false, |
|
||||
// userObject.optBoolean("is_verified"), |
|
||||
// userObject.getString("id"), |
|
||||
// userObject.getString("username"), |
|
||||
// userObject.optString("full_name"), |
|
||||
// null, null, |
|
||||
// userObject.getString("profile_pic_url"), |
|
||||
// null, 0, 0, 0, false, false, false, false, false)); |
|
||||
} |
|
||||
callback.onSuccess(new GraphQLUserListFetchResponse(endCursor, status, userModels)); |
|
||||
} catch (JSONException e) { |
|
||||
Log.e(TAG, "onResponse", e); |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(e); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public Call<String> fetchComments(final String shortCodeOrCommentId, |
|
||||
final boolean root, |
|
||||
final String cursor) { |
|
||||
final Map<String, String> queryMap = new HashMap<>(); |
|
||||
queryMap.put("query_hash", root ? "bc3296d1ce80a24b1b6e40b1e72903f5" : "51fdd02b67508306ad4484ff574a0b62"); |
|
||||
final Map<String, Object> variables = ImmutableMap.of( |
|
||||
root ? "shortcode" : "comment_id", shortCodeOrCommentId, |
|
||||
"first", 50, |
|
||||
"after", cursor == null ? "" : cursor |
|
||||
); |
|
||||
queryMap.put("variables", new JSONObject(variables).toString()); |
|
||||
return repository.fetch(queryMap); |
|
||||
} |
|
||||
|
|
||||
// TODO convert string response to a response class |
|
||||
public void fetchUser(final String username, |
|
||||
final ServiceCallback<User> callback) { |
|
||||
final Call<String> request = repository.getUser(username); |
|
||||
request.enqueue(new Callback<String>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { |
|
||||
final String rawBody = response.body(); |
|
||||
if (rawBody == null) { |
|
||||
Log.e(TAG, "Error occurred while fetching gql user of " + username); |
|
||||
callback.onSuccess(null); |
|
||||
return; |
|
||||
} |
|
||||
try { |
|
||||
final JSONObject body = new JSONObject(rawBody); |
|
||||
final JSONObject userJson = body.getJSONObject("graphql") |
|
||||
.getJSONObject(Constants.EXTRAS_USER); |
|
||||
|
|
||||
boolean isPrivate = userJson.getBoolean("is_private"); |
|
||||
final long id = userJson.optLong(Constants.EXTRAS_ID, 0); |
|
||||
final JSONObject timelineMedia = userJson.getJSONObject("edge_owner_to_timeline_media"); |
|
||||
// if (timelineMedia.has("edges")) { |
|
||||
// final JSONArray edges = timelineMedia.getJSONArray("edges"); |
|
||||
// } |
|
||||
|
|
||||
String url = userJson.optString("external_url"); |
|
||||
if (TextUtils.isEmpty(url)) url = null; |
|
||||
|
|
||||
callback.onSuccess(new User( |
|
||||
id, |
|
||||
username, |
|
||||
userJson.getString("full_name"), |
|
||||
isPrivate, |
|
||||
userJson.getString("profile_pic_url_hd"), |
|
||||
null, |
|
||||
new FriendshipStatus( |
|
||||
userJson.optBoolean("followed_by_viewer"), |
|
||||
userJson.optBoolean("follows_viewer"), |
|
||||
userJson.optBoolean("blocked_by_viewer"), |
|
||||
false, |
|
||||
isPrivate, |
|
||||
userJson.optBoolean("has_requested_viewer"), |
|
||||
userJson.optBoolean("requested_by_viewer"), |
|
||||
false, |
|
||||
userJson.optBoolean("restricted_by_viewer"), |
|
||||
false |
|
||||
), |
|
||||
userJson.getBoolean("is_verified"), |
|
||||
false, |
|
||||
false, |
|
||||
false, |
|
||||
false, |
|
||||
false, |
|
||||
null, |
|
||||
null, |
|
||||
timelineMedia.getLong("count"), |
|
||||
userJson.getJSONObject("edge_followed_by").getLong("count"), |
|
||||
userJson.getJSONObject("edge_follow").getLong("count"), |
|
||||
0, |
|
||||
userJson.getString("biography"), |
|
||||
url, |
|
||||
0, |
|
||||
null, |
|
||||
null, |
|
||||
null, |
|
||||
null, |
|
||||
null, |
|
||||
null)); |
|
||||
} catch (JSONException e) { |
|
||||
Log.e(TAG, "onResponse", e); |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(e); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// TODO convert string response to a response class |
|
||||
public void fetchPost(final String shortcode, |
|
||||
final ServiceCallback<Media> callback) { |
|
||||
final Call<String> request = repository.getPost(shortcode); |
|
||||
request.enqueue(new Callback<String>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { |
|
||||
final String rawBody = response.body(); |
|
||||
if (rawBody == null) { |
|
||||
Log.e(TAG, "Error occurred while fetching gql post of " + shortcode); |
|
||||
callback.onSuccess(null); |
|
||||
return; |
|
||||
} |
|
||||
try { |
|
||||
final JSONObject body = new JSONObject(rawBody); |
|
||||
final JSONObject media = body.getJSONObject("graphql") |
|
||||
.getJSONObject("shortcode_media"); |
|
||||
callback.onSuccess(ResponseBodyUtils.parseGraphQLItem(media, null)); |
|
||||
} catch (JSONException e) { |
|
||||
Log.e(TAG, "onResponse", e); |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(e); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// TODO convert string response to a response class |
|
||||
public void fetchTag(final String tag, |
|
||||
final ServiceCallback<Hashtag> callback) { |
|
||||
final Call<String> request = repository.getTag(tag); |
|
||||
request.enqueue(new Callback<String>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { |
|
||||
final String rawBody = response.body(); |
|
||||
if (rawBody == null) { |
|
||||
Log.e(TAG, "Error occurred while fetching gql tag of " + tag); |
|
||||
callback.onSuccess(null); |
|
||||
return; |
|
||||
} |
|
||||
try { |
|
||||
final JSONObject body = new JSONObject(rawBody) |
|
||||
.getJSONObject("graphql") |
|
||||
.getJSONObject(Constants.EXTRAS_HASHTAG); |
|
||||
final JSONObject timelineMedia = body.getJSONObject("edge_hashtag_to_media"); |
|
||||
callback.onSuccess(new Hashtag( |
|
||||
body.getString(Constants.EXTRAS_ID), |
|
||||
body.getString("name"), |
|
||||
timelineMedia.getLong("count"), |
|
||||
body.optBoolean("is_following") ? FollowingType.FOLLOWING : FollowingType.NOT_FOLLOWING, |
|
||||
null)); |
|
||||
} catch (JSONException e) { |
|
||||
Log.e(TAG, "onResponse", e); |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(e); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// TODO convert string response to a response class |
|
||||
public void fetchLocation(final long locationId, |
|
||||
final ServiceCallback<Location> callback) { |
|
||||
final Call<String> request = repository.getLocation(locationId); |
|
||||
request.enqueue(new Callback<String>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { |
|
||||
final String rawBody = response.body(); |
|
||||
if (rawBody == null) { |
|
||||
Log.e(TAG, "Error occurred while fetching gql location of " + locationId); |
|
||||
callback.onSuccess(null); |
|
||||
return; |
|
||||
} |
|
||||
try { |
|
||||
final JSONObject body = new JSONObject(rawBody) |
|
||||
.getJSONObject("graphql") |
|
||||
.getJSONObject(Constants.EXTRAS_LOCATION); |
|
||||
final JSONObject timelineMedia = body.getJSONObject("edge_location_to_media"); |
|
||||
final JSONObject address = new JSONObject(body.getString("address_json")); |
|
||||
callback.onSuccess(new Location( |
|
||||
body.getLong(Constants.EXTRAS_ID), |
|
||||
body.getString("slug"), |
|
||||
body.getString("name"), |
|
||||
address.optString("street_address"), |
|
||||
address.optString("city_name"), |
|
||||
body.optDouble("lng", 0d), |
|
||||
body.optDouble("lat", 0d) |
|
||||
)); |
|
||||
} catch (JSONException e) { |
|
||||
Log.e(TAG, "onResponse", e); |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(e); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,266 @@ |
|||||
|
package awais.instagrabber.webservices |
||||
|
|
||||
|
import android.util.Log |
||||
|
import awais.instagrabber.models.enums.FollowingType |
||||
|
import awais.instagrabber.repositories.GraphQLRepository |
||||
|
import awais.instagrabber.repositories.responses.* |
||||
|
import awais.instagrabber.utils.Constants |
||||
|
import awais.instagrabber.utils.ResponseBodyUtils |
||||
|
import awais.instagrabber.utils.extensions.TAG |
||||
|
import awais.instagrabber.webservices.RetrofitFactory.retrofitWeb |
||||
|
import org.json.JSONException |
||||
|
import org.json.JSONObject |
||||
|
import java.util.* |
||||
|
|
||||
|
object GraphQLService : BaseService() { |
||||
|
private val repository: GraphQLRepository = retrofitWeb.create(GraphQLRepository::class.java) |
||||
|
|
||||
|
// TODO convert string response to a response class |
||||
|
private suspend fun fetch( |
||||
|
queryHash: String, |
||||
|
variables: String, |
||||
|
arg1: String, |
||||
|
arg2: String, |
||||
|
backup: User?, |
||||
|
): PostsFetchResponse { |
||||
|
val queryMap = mapOf( |
||||
|
"query_hash" to queryHash, |
||||
|
"variables" to variables, |
||||
|
) |
||||
|
val response = repository.fetch(queryMap) |
||||
|
return parsePostResponse(response, arg1, arg2, backup) |
||||
|
} |
||||
|
|
||||
|
suspend fun fetchLocationPosts( |
||||
|
locationId: Long, |
||||
|
maxId: String?, |
||||
|
): PostsFetchResponse = fetch( |
||||
|
"36bd0f2bf5911908de389b8ceaa3be6d", |
||||
|
"{\"id\":\"" + locationId + "\"," + "\"first\":25," + "\"after\":\"" + (maxId ?: "") + "\"}", |
||||
|
Constants.EXTRAS_LOCATION, |
||||
|
"edge_location_to_media", |
||||
|
null |
||||
|
) |
||||
|
|
||||
|
suspend fun fetchHashtagPosts( |
||||
|
tag: String, |
||||
|
maxId: String?, |
||||
|
): PostsFetchResponse = fetch( |
||||
|
"9b498c08113f1e09617a1703c22b2f32", |
||||
|
"{\"tag_name\":\"" + tag + "\"," + "\"first\":25," + "\"after\":\"" + (maxId ?: "") + "\"}", |
||||
|
Constants.EXTRAS_HASHTAG, |
||||
|
"edge_hashtag_to_media", |
||||
|
null, |
||||
|
) |
||||
|
|
||||
|
suspend fun fetchProfilePosts( |
||||
|
profileId: Long, |
||||
|
postsPerPage: Int, |
||||
|
maxId: String?, |
||||
|
backup: User?, |
||||
|
): PostsFetchResponse = fetch( |
||||
|
"02e14f6a7812a876f7d133c9555b1151", |
||||
|
"{\"id\":\"" + profileId + "\"," + "\"first\":" + postsPerPage + "," + "\"after\":\"" + (maxId ?: "") + "\"}", |
||||
|
Constants.EXTRAS_USER, |
||||
|
"edge_owner_to_timeline_media", |
||||
|
backup, |
||||
|
) |
||||
|
|
||||
|
suspend fun fetchTaggedPosts( |
||||
|
profileId: Long, |
||||
|
postsPerPage: Int, |
||||
|
maxId: String?, |
||||
|
): PostsFetchResponse = fetch( |
||||
|
"31fe64d9463cbbe58319dced405c6206", |
||||
|
"{\"id\":\"" + profileId + "\"," + "\"first\":" + postsPerPage + "," + "\"after\":\"" + (maxId ?: "") + "\"}", |
||||
|
Constants.EXTRAS_USER, |
||||
|
"edge_user_to_photos_of_you", |
||||
|
null, |
||||
|
) |
||||
|
|
||||
|
@Throws(JSONException::class) |
||||
|
private fun parsePostResponse( |
||||
|
response: String, |
||||
|
arg1: String, |
||||
|
arg2: String, |
||||
|
backup: User?, |
||||
|
): PostsFetchResponse { |
||||
|
if (response.isBlank()) { |
||||
|
Log.e(TAG, "parseResponse: feed response body is empty") |
||||
|
return PostsFetchResponse(emptyList(), false, null) |
||||
|
} |
||||
|
return parseResponseBody(response, arg1, arg2, backup) |
||||
|
} |
||||
|
|
||||
|
@Throws(JSONException::class) |
||||
|
private fun parseResponseBody( |
||||
|
body: String, |
||||
|
arg1: String, |
||||
|
arg2: String, |
||||
|
backup: User?, |
||||
|
): PostsFetchResponse { |
||||
|
val items: MutableList<Media> = ArrayList() |
||||
|
val timelineFeed = JSONObject(body) |
||||
|
.getJSONObject("data") |
||||
|
.getJSONObject(arg1) |
||||
|
.getJSONObject(arg2) |
||||
|
val endCursor: String? |
||||
|
val hasNextPage: Boolean |
||||
|
val pageInfo = timelineFeed.getJSONObject("page_info") |
||||
|
if (pageInfo.has("has_next_page")) { |
||||
|
hasNextPage = pageInfo.getBoolean("has_next_page") |
||||
|
endCursor = if (hasNextPage) pageInfo.getString("end_cursor") else null |
||||
|
} else { |
||||
|
hasNextPage = false |
||||
|
endCursor = null |
||||
|
} |
||||
|
val feedItems = timelineFeed.getJSONArray("edges") |
||||
|
for (i in 0 until feedItems.length()) { |
||||
|
val itemJson = feedItems.optJSONObject(i) ?: continue |
||||
|
val media = ResponseBodyUtils.parseGraphQLItem(itemJson, backup) |
||||
|
if (media != null) { |
||||
|
items.add(media) |
||||
|
} |
||||
|
} |
||||
|
return PostsFetchResponse(items, hasNextPage, endCursor) |
||||
|
} |
||||
|
|
||||
|
// TODO convert string response to a response class |
||||
|
suspend fun fetchCommentLikers( |
||||
|
commentId: String, |
||||
|
endCursor: String?, |
||||
|
): GraphQLUserListFetchResponse { |
||||
|
val queryMap = mapOf( |
||||
|
"query_hash" to "5f0b1f6281e72053cbc07909c8d154ae", |
||||
|
"variables" to "{\"comment_id\":\"" + commentId + "\"," + "\"first\":30," + "\"after\":\"" + (endCursor ?: "") + "\"}" |
||||
|
) |
||||
|
val response = repository.fetch(queryMap) |
||||
|
val body = JSONObject(response) |
||||
|
val status = body.getString("status") |
||||
|
val data = body.getJSONObject("data").getJSONObject("comment").getJSONObject("edge_liked_by") |
||||
|
val pageInfo = data.getJSONObject("page_info") |
||||
|
val newEndCursor = if (pageInfo.getBoolean("has_next_page")) pageInfo.getString("end_cursor") else null |
||||
|
val users = data.getJSONArray("edges") |
||||
|
val usersLen = users.length() |
||||
|
val userModels: MutableList<User> = ArrayList() |
||||
|
for (j in 0 until usersLen) { |
||||
|
val userObject = users.getJSONObject(j).getJSONObject("node") |
||||
|
userModels.add(User( |
||||
|
userObject.getLong("id"), |
||||
|
userObject.getString("username"), |
||||
|
userObject.optString("full_name"), |
||||
|
userObject.optBoolean("is_private"), |
||||
|
userObject.getString("profile_pic_url"), |
||||
|
userObject.optBoolean("is_verified") |
||||
|
)) |
||||
|
} |
||||
|
return GraphQLUserListFetchResponse(newEndCursor, status, userModels) |
||||
|
} |
||||
|
|
||||
|
suspend fun fetchComments( |
||||
|
shortCodeOrCommentId: String?, |
||||
|
root: Boolean, |
||||
|
cursor: String?, |
||||
|
): String { |
||||
|
val variables = mapOf( |
||||
|
(if (root) "shortcode" else "comment_id") to shortCodeOrCommentId, |
||||
|
"first" to 50, |
||||
|
"after" to (cursor ?: "") |
||||
|
) |
||||
|
val queryMap = mapOf( |
||||
|
"query_hash" to if (root) "bc3296d1ce80a24b1b6e40b1e72903f5" else "51fdd02b67508306ad4484ff574a0b62", |
||||
|
"variables" to JSONObject(variables).toString() |
||||
|
) |
||||
|
return repository.fetch(queryMap) |
||||
|
} |
||||
|
|
||||
|
// TODO convert string response to a response class |
||||
|
suspend fun fetchUser( |
||||
|
username: String, |
||||
|
): User { |
||||
|
val response = repository.getUser(username) |
||||
|
val body = JSONObject(response) |
||||
|
val userJson = body.getJSONObject("graphql").getJSONObject(Constants.EXTRAS_USER) |
||||
|
val isPrivate = userJson.getBoolean("is_private") |
||||
|
val id = userJson.optLong(Constants.EXTRAS_ID, 0) |
||||
|
val timelineMedia = userJson.getJSONObject("edge_owner_to_timeline_media") |
||||
|
// if (timelineMedia.has("edges")) { |
||||
|
// final JSONArray edges = timelineMedia.getJSONArray("edges"); |
||||
|
// } |
||||
|
var url: String? = userJson.optString("external_url") |
||||
|
if (url.isNullOrBlank()) url = null |
||||
|
return User( |
||||
|
id, |
||||
|
username, |
||||
|
userJson.getString("full_name"), |
||||
|
isPrivate, |
||||
|
userJson.getString("profile_pic_url_hd"), |
||||
|
userJson.getBoolean("is_verified"), |
||||
|
friendshipStatus = FriendshipStatus( |
||||
|
userJson.optBoolean("followed_by_viewer"), |
||||
|
userJson.optBoolean("follows_viewer"), |
||||
|
userJson.optBoolean("blocked_by_viewer"), |
||||
|
false, |
||||
|
isPrivate, |
||||
|
userJson.optBoolean("has_requested_viewer"), |
||||
|
userJson.optBoolean("requested_by_viewer"), |
||||
|
false, |
||||
|
userJson.optBoolean("restricted_by_viewer"), |
||||
|
false |
||||
|
), |
||||
|
mediaCount = timelineMedia.getLong("count"), |
||||
|
followerCount = userJson.getJSONObject("edge_followed_by").getLong("count"), |
||||
|
followingCount = userJson.getJSONObject("edge_follow").getLong("count"), |
||||
|
biography = userJson.getString("biography"), |
||||
|
externalUrl = url, |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// TODO convert string response to a response class |
||||
|
suspend fun fetchPost( |
||||
|
shortcode: String, |
||||
|
): Media { |
||||
|
val response = repository.getPost(shortcode) |
||||
|
val body = JSONObject(response) |
||||
|
val media = body.getJSONObject("graphql").getJSONObject("shortcode_media") |
||||
|
return ResponseBodyUtils.parseGraphQLItem(media, null) |
||||
|
} |
||||
|
|
||||
|
// TODO convert string response to a response class |
||||
|
suspend fun fetchTag( |
||||
|
tag: String, |
||||
|
): Hashtag { |
||||
|
val response = repository.getTag(tag) |
||||
|
val body = JSONObject(response) |
||||
|
.getJSONObject("graphql") |
||||
|
.getJSONObject(Constants.EXTRAS_HASHTAG) |
||||
|
val timelineMedia = body.getJSONObject("edge_hashtag_to_media") |
||||
|
return Hashtag( |
||||
|
body.getString(Constants.EXTRAS_ID), |
||||
|
body.getString("name"), |
||||
|
timelineMedia.getLong("count"), |
||||
|
if (body.optBoolean("is_following")) FollowingType.FOLLOWING else FollowingType.NOT_FOLLOWING, |
||||
|
null) |
||||
|
} |
||||
|
|
||||
|
// TODO convert string response to a response class |
||||
|
suspend fun fetchLocation( |
||||
|
locationId: Long, |
||||
|
): Location { |
||||
|
val response = repository.getLocation(locationId) |
||||
|
val body = JSONObject(response) |
||||
|
.getJSONObject("graphql") |
||||
|
.getJSONObject(Constants.EXTRAS_LOCATION) |
||||
|
// val timelineMedia = body.getJSONObject("edge_location_to_media") |
||||
|
val address = JSONObject(body.getString("address_json")) |
||||
|
return Location( |
||||
|
body.getLong(Constants.EXTRAS_ID), |
||||
|
body.getString("slug"), |
||||
|
body.getString("name"), |
||||
|
address.optString("street_address"), |
||||
|
address.optString("city_name"), |
||||
|
body.optDouble("lng", 0.0), |
||||
|
body.optDouble("lat", 0.0) |
||||
|
) |
||||
|
} |
||||
|
} |
@ -1,548 +0,0 @@ |
|||||
package awais.instagrabber.webservices; |
|
||||
|
|
||||
import android.util.Log; |
|
||||
|
|
||||
import androidx.annotation.NonNull; |
|
||||
import androidx.annotation.Nullable; |
|
||||
|
|
||||
import org.json.JSONArray; |
|
||||
import org.json.JSONException; |
|
||||
import org.json.JSONObject; |
|
||||
|
|
||||
import java.util.ArrayList; |
|
||||
import java.util.Collections; |
|
||||
import java.util.HashMap; |
|
||||
import java.util.List; |
|
||||
import java.util.Map; |
|
||||
import java.util.Objects; |
|
||||
import java.util.UUID; |
|
||||
|
|
||||
import awais.instagrabber.fragments.settings.PreferenceKeys; |
|
||||
import awais.instagrabber.models.FeedStoryModel; |
|
||||
import awais.instagrabber.models.HighlightModel; |
|
||||
import awais.instagrabber.models.StoryModel; |
|
||||
import awais.instagrabber.repositories.StoriesRepository; |
|
||||
import awais.instagrabber.repositories.requests.StoryViewerOptions; |
|
||||
import awais.instagrabber.repositories.responses.StoryStickerResponse; |
|
||||
import awais.instagrabber.repositories.responses.User; |
|
||||
import awais.instagrabber.utils.Constants; |
|
||||
import awais.instagrabber.utils.ResponseBodyUtils; |
|
||||
import awais.instagrabber.utils.TextUtils; |
|
||||
import awais.instagrabber.utils.Utils; |
|
||||
import retrofit2.Call; |
|
||||
import retrofit2.Callback; |
|
||||
import retrofit2.Response; |
|
||||
|
|
||||
public class StoriesService extends BaseService { |
|
||||
private static final String TAG = "StoriesService"; |
|
||||
|
|
||||
private static StoriesService instance; |
|
||||
|
|
||||
private final StoriesRepository repository; |
|
||||
private final String csrfToken; |
|
||||
private final long userId; |
|
||||
private final String deviceUuid; |
|
||||
|
|
||||
private StoriesService(@NonNull final String csrfToken, |
|
||||
final long userId, |
|
||||
@NonNull final String deviceUuid) { |
|
||||
this.csrfToken = csrfToken; |
|
||||
this.userId = userId; |
|
||||
this.deviceUuid = deviceUuid; |
|
||||
repository = RetrofitFactory.INSTANCE |
|
||||
.getRetrofit() |
|
||||
.create(StoriesRepository.class); |
|
||||
} |
|
||||
|
|
||||
public String getCsrfToken() { |
|
||||
return csrfToken; |
|
||||
} |
|
||||
|
|
||||
public long getUserId() { |
|
||||
return userId; |
|
||||
} |
|
||||
|
|
||||
public String getDeviceUuid() { |
|
||||
return deviceUuid; |
|
||||
} |
|
||||
|
|
||||
public static StoriesService getInstance(final String csrfToken, |
|
||||
final long userId, |
|
||||
final String deviceUuid) { |
|
||||
if (instance == null |
|
||||
|| !Objects.equals(instance.getCsrfToken(), csrfToken) |
|
||||
|| !Objects.equals(instance.getUserId(), userId) |
|
||||
|| !Objects.equals(instance.getDeviceUuid(), deviceUuid)) { |
|
||||
instance = new StoriesService(csrfToken, userId, deviceUuid); |
|
||||
} |
|
||||
return instance; |
|
||||
} |
|
||||
|
|
||||
public void fetch(final long mediaId, |
|
||||
final ServiceCallback<StoryModel> callback) { |
|
||||
final Call<String> request = repository.fetch(mediaId); |
|
||||
request.enqueue(new Callback<String>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<String> call, |
|
||||
@NonNull final Response<String> response) { |
|
||||
if (callback == null) return; |
|
||||
final String body = response.body(); |
|
||||
if (body == null) { |
|
||||
callback.onSuccess(null); |
|
||||
return; |
|
||||
} |
|
||||
try { |
|
||||
final JSONObject itemJson = new JSONObject(body).getJSONArray("items").getJSONObject(0); |
|
||||
callback.onSuccess(ResponseBodyUtils.parseStoryItem(itemJson, false, null)); |
|
||||
} catch (JSONException e) { |
|
||||
callback.onFailure(e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<String> call, |
|
||||
@NonNull final Throwable t) { |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public void getFeedStories(final ServiceCallback<List<FeedStoryModel>> callback) { |
|
||||
final Call<String> response = repository.getFeedStories(); |
|
||||
response.enqueue(new Callback<String>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { |
|
||||
final String body = response.body(); |
|
||||
if (body == null) { |
|
||||
Log.e(TAG, "getFeedStories: body is empty"); |
|
||||
return; |
|
||||
} |
|
||||
parseStoriesBody(body, callback); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private void parseStoriesBody(final String body, final ServiceCallback<List<FeedStoryModel>> callback) { |
|
||||
try { |
|
||||
final List<FeedStoryModel> feedStoryModels = new ArrayList<>(); |
|
||||
final JSONArray feedStoriesReel = new JSONObject(body).getJSONArray("tray"); |
|
||||
for (int i = 0; i < feedStoriesReel.length(); ++i) { |
|
||||
final JSONObject node = feedStoriesReel.getJSONObject(i); |
|
||||
if (node.optBoolean("hide_from_feed_unit") && Utils.settingsHelper.getBoolean(PreferenceKeys.HIDE_MUTED_REELS)) continue; |
|
||||
final JSONObject userJson = node.getJSONObject(node.has("user") ? "user" : "owner"); |
|
||||
try { |
|
||||
final User user = new User(userJson.getLong("pk"), |
|
||||
userJson.getString("username"), |
|
||||
userJson.optString("full_name"), |
|
||||
userJson.optBoolean("is_private"), |
|
||||
userJson.getString("profile_pic_url"), |
|
||||
userJson.optBoolean("is_verified") |
|
||||
); |
|
||||
final long timestamp = node.getLong("latest_reel_media"); |
|
||||
final boolean fullyRead = !node.isNull("seen") && node.getLong("seen") == timestamp; |
|
||||
final JSONObject itemJson = node.has("items") ? node.getJSONArray("items").optJSONObject(0) : null; |
|
||||
StoryModel firstStoryModel = null; |
|
||||
if (itemJson != null) { |
|
||||
firstStoryModel = ResponseBodyUtils.parseStoryItem(itemJson, false, null); |
|
||||
} |
|
||||
feedStoryModels.add(new FeedStoryModel( |
|
||||
node.getString("id"), |
|
||||
user, |
|
||||
fullyRead, |
|
||||
timestamp, |
|
||||
firstStoryModel, |
|
||||
node.getInt("media_count"), |
|
||||
false, |
|
||||
node.optBoolean("has_besties_media"))); |
|
||||
} catch (Exception e) { |
|
||||
Log.e(TAG, "parseStoriesBody: ", e); |
|
||||
} // to cover promotional reels with non-long user pk's |
|
||||
} |
|
||||
final JSONArray broadcasts = new JSONObject(body).getJSONArray("broadcasts"); |
|
||||
for (int i = 0; i < broadcasts.length(); ++i) { |
|
||||
final JSONObject node = broadcasts.getJSONObject(i); |
|
||||
final JSONObject userJson = node.getJSONObject("broadcast_owner"); |
|
||||
// final ProfileModel profileModel = new ProfileModel(false, false, false, |
|
||||
// userJson.getString("pk"), |
|
||||
// userJson.getString("username"), |
|
||||
// null, null, null, |
|
||||
// userJson.getString("profile_pic_url"), |
|
||||
// null, 0, 0, 0, false, false, false, false, false); |
|
||||
final User user = new User(userJson.getLong("pk"), |
|
||||
userJson.getString("username"), |
|
||||
userJson.optString("full_name"), |
|
||||
userJson.optBoolean("is_private"), |
|
||||
userJson.getString("profile_pic_url"), |
|
||||
userJson.optBoolean("is_verified") |
|
||||
); |
|
||||
feedStoryModels.add(new FeedStoryModel( |
|
||||
node.getString("id"), |
|
||||
user, |
|
||||
false, |
|
||||
node.getLong("published_time"), |
|
||||
ResponseBodyUtils.parseBroadcastItem(node), |
|
||||
1, |
|
||||
true, |
|
||||
false |
|
||||
)); |
|
||||
} |
|
||||
callback.onSuccess(sort(feedStoryModels)); |
|
||||
} catch (JSONException e) { |
|
||||
Log.e(TAG, "Error parsing json", e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void fetchHighlights(final long profileId, |
|
||||
final ServiceCallback<List<HighlightModel>> callback) { |
|
||||
final Call<String> request = repository.fetchHighlights(profileId); |
|
||||
request.enqueue(new Callback<String>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { |
|
||||
try { |
|
||||
if (callback == null) { |
|
||||
return; |
|
||||
} |
|
||||
final String body = response.body(); |
|
||||
if (TextUtils.isEmpty(body)) { |
|
||||
callback.onSuccess(null); |
|
||||
return; |
|
||||
} |
|
||||
final JSONArray highlightsReel = new JSONObject(body).getJSONArray("tray"); |
|
||||
|
|
||||
final int length = highlightsReel.length(); |
|
||||
final List<HighlightModel> highlightModels = new ArrayList<>(); |
|
||||
|
|
||||
for (int i = 0; i < length; ++i) { |
|
||||
final JSONObject highlightNode = highlightsReel.getJSONObject(i); |
|
||||
highlightModels.add(new HighlightModel( |
|
||||
highlightNode.getString("title"), |
|
||||
highlightNode.getString(Constants.EXTRAS_ID), |
|
||||
highlightNode.getJSONObject("cover_media") |
|
||||
.getJSONObject("cropped_image_version") |
|
||||
.getString("url"), |
|
||||
highlightNode.getLong("latest_reel_media"), |
|
||||
highlightNode.getInt("media_count") |
|
||||
)); |
|
||||
} |
|
||||
callback.onSuccess(highlightModels); |
|
||||
} catch (JSONException e) { |
|
||||
Log.e(TAG, "onResponse", e); |
|
||||
callback.onFailure(e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public void fetchArchive(final String maxId, |
|
||||
final ServiceCallback<ArchiveFetchResponse> callback) { |
|
||||
final Map<String, String> form = new HashMap<>(); |
|
||||
form.put("include_suggested_highlights", "false"); |
|
||||
form.put("is_in_archive_home", "true"); |
|
||||
form.put("include_cover", "1"); |
|
||||
if (!TextUtils.isEmpty(maxId)) { |
|
||||
form.put("max_id", maxId); // NOT TESTED |
|
||||
} |
|
||||
final Call<String> request = repository.fetchArchive(form); |
|
||||
request.enqueue(new Callback<String>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { |
|
||||
try { |
|
||||
if (callback == null) { |
|
||||
return; |
|
||||
} |
|
||||
final String body = response.body(); |
|
||||
if (TextUtils.isEmpty(body)) { |
|
||||
callback.onSuccess(null); |
|
||||
return; |
|
||||
} |
|
||||
final JSONObject data = new JSONObject(body); |
|
||||
final JSONArray highlightsReel = data.getJSONArray("items"); |
|
||||
|
|
||||
final int length = highlightsReel.length(); |
|
||||
final List<HighlightModel> highlightModels = new ArrayList<>(); |
|
||||
|
|
||||
for (int i = 0; i < length; ++i) { |
|
||||
final JSONObject highlightNode = highlightsReel.getJSONObject(i); |
|
||||
highlightModels.add(new HighlightModel( |
|
||||
null, |
|
||||
highlightNode.getString(Constants.EXTRAS_ID), |
|
||||
highlightNode.getJSONObject("cover_image_version").getString("url"), |
|
||||
highlightNode.getLong("latest_reel_media"), |
|
||||
highlightNode.getInt("media_count") |
|
||||
)); |
|
||||
} |
|
||||
callback.onSuccess(new ArchiveFetchResponse(highlightModels, |
|
||||
data.getBoolean("more_available"), |
|
||||
data.getString("max_id"))); |
|
||||
} catch (JSONException e) { |
|
||||
Log.e(TAG, "onResponse", e); |
|
||||
callback.onFailure(e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public void getUserStory(final StoryViewerOptions options, |
|
||||
final ServiceCallback<List<StoryModel>> callback) { |
|
||||
final String url = buildUrl(options); |
|
||||
final Call<String> userStoryCall = repository.getUserStory(url); |
|
||||
final boolean isLocOrHashtag = options.getType() == StoryViewerOptions.Type.LOCATION || options.getType() == StoryViewerOptions.Type.HASHTAG; |
|
||||
final boolean isHighlight = options.getType() == StoryViewerOptions.Type.HIGHLIGHT || options |
|
||||
.getType() == StoryViewerOptions.Type.STORY_ARCHIVE; |
|
||||
userStoryCall.enqueue(new Callback<String>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { |
|
||||
JSONObject data; |
|
||||
try { |
|
||||
final String body = response.body(); |
|
||||
if (body == null) { |
|
||||
Log.e(TAG, "body is null"); |
|
||||
return; |
|
||||
} |
|
||||
data = new JSONObject(body); |
|
||||
|
|
||||
if (!isHighlight) { |
|
||||
data = data.optJSONObject((isLocOrHashtag) ? "story" : "reel"); |
|
||||
} else { |
|
||||
data = data.getJSONObject("reels").optJSONObject(options.getName()); |
|
||||
} |
|
||||
|
|
||||
String username = null; |
|
||||
if (data != null |
|
||||
// && localUsername == null |
|
||||
&& !isLocOrHashtag) { |
|
||||
username = data.getJSONObject("user").getString("username"); |
|
||||
} |
|
||||
|
|
||||
JSONArray media; |
|
||||
if (data != null |
|
||||
&& (media = data.optJSONArray("items")) != null |
|
||||
&& media.length() > 0 && media.optJSONObject(0) != null) { |
|
||||
final int mediaLen = media.length(); |
|
||||
final List<StoryModel> models = new ArrayList<>(); |
|
||||
for (int i = 0; i < mediaLen; ++i) { |
|
||||
data = media.getJSONObject(i); |
|
||||
models.add(ResponseBodyUtils.parseStoryItem(data, isLocOrHashtag, username)); |
|
||||
} |
|
||||
callback.onSuccess(models); |
|
||||
} else { |
|
||||
callback.onSuccess(null); |
|
||||
} |
|
||||
} catch (JSONException e) { |
|
||||
Log.e(TAG, "Error parsing string", e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private void respondToSticker(final String storyId, |
|
||||
final String stickerId, |
|
||||
final String action, |
|
||||
final String arg1, |
|
||||
final String arg2, |
|
||||
final ServiceCallback<StoryStickerResponse> callback) { |
|
||||
final Map<String, Object> form = new HashMap<>(); |
|
||||
form.put("_csrftoken", csrfToken); |
|
||||
form.put("_uid", userId); |
|
||||
form.put("_uuid", deviceUuid); |
|
||||
form.put("mutation_token", UUID.randomUUID().toString()); |
|
||||
form.put("client_context", UUID.randomUUID().toString()); |
|
||||
form.put("radio_type", "wifi-none"); |
|
||||
form.put(arg1, arg2); |
|
||||
final Map<String, String> signedForm = Utils.sign(form); |
|
||||
final Call<StoryStickerResponse> request = |
|
||||
repository.respondToSticker(storyId, stickerId, action, signedForm); |
|
||||
request.enqueue(new Callback<StoryStickerResponse>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<StoryStickerResponse> call, |
|
||||
@NonNull final Response<StoryStickerResponse> response) { |
|
||||
if (callback != null) { |
|
||||
callback.onSuccess(response.body()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<StoryStickerResponse> call, |
|
||||
@NonNull final Throwable t) { |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// RespondAction.java |
|
||||
public void respondToQuestion(final String storyId, |
|
||||
final String stickerId, |
|
||||
final String answer, |
|
||||
final ServiceCallback<StoryStickerResponse> callback) { |
|
||||
respondToSticker(storyId, stickerId, "story_question_response", "response", answer, callback); |
|
||||
} |
|
||||
|
|
||||
// QuizAction.java |
|
||||
public void respondToQuiz(final String storyId, |
|
||||
final String stickerId, |
|
||||
final int answer, |
|
||||
final ServiceCallback<StoryStickerResponse> callback) { |
|
||||
respondToSticker(storyId, stickerId, "story_quiz_answer", "answer", String.valueOf(answer), callback); |
|
||||
} |
|
||||
|
|
||||
// VoteAction.java |
|
||||
public void respondToPoll(final String storyId, |
|
||||
final String stickerId, |
|
||||
final int answer, |
|
||||
final ServiceCallback<StoryStickerResponse> callback) { |
|
||||
respondToSticker(storyId, stickerId, "story_poll_vote", "vote", String.valueOf(answer), callback); |
|
||||
} |
|
||||
|
|
||||
public void respondToSlider(final String storyId, |
|
||||
final String stickerId, |
|
||||
final double answer, |
|
||||
final ServiceCallback<StoryStickerResponse> callback) { |
|
||||
respondToSticker(storyId, stickerId, "story_slider_vote", "vote", String.valueOf(answer), callback); |
|
||||
} |
|
||||
|
|
||||
public void seen(final String storyMediaId, |
|
||||
final long takenAt, |
|
||||
final long seenAt, |
|
||||
final ServiceCallback<String> callback) { |
|
||||
final Map<String, Object> form = new HashMap<>(); |
|
||||
form.put("_csrftoken", csrfToken); |
|
||||
form.put("_uid", userId); |
|
||||
form.put("_uuid", deviceUuid); |
|
||||
form.put("container_module", "feed_timeline"); |
|
||||
final Map<String, Object> reelsForm = new HashMap<>(); |
|
||||
reelsForm.put(storyMediaId, Collections.singletonList(takenAt + "_" + seenAt)); |
|
||||
form.put("reels", reelsForm); |
|
||||
final Map<String, String> signedForm = Utils.sign(form); |
|
||||
final Map<String, String> queryMap = new HashMap<>(); |
|
||||
queryMap.put("reel", "1"); |
|
||||
queryMap.put("live_vod", "0"); |
|
||||
final Call<String> request = repository.seen(queryMap, signedForm); |
|
||||
request.enqueue(new Callback<String>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<String> call, |
|
||||
@NonNull final Response<String> response) { |
|
||||
if (callback != null) { |
|
||||
callback.onSuccess(response.body()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<String> call, |
|
||||
@NonNull final Throwable t) { |
|
||||
if (callback != null) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
@Nullable |
|
||||
private String buildUrl(@NonNull final StoryViewerOptions options) { |
|
||||
final StringBuilder builder = new StringBuilder(); |
|
||||
builder.append("https://i.instagram.com/api/v1/"); |
|
||||
final StoryViewerOptions.Type type = options.getType(); |
|
||||
String id = null; |
|
||||
switch (type) { |
|
||||
case HASHTAG: |
|
||||
builder.append("tags/"); |
|
||||
id = options.getName(); |
|
||||
break; |
|
||||
case LOCATION: |
|
||||
builder.append("locations/"); |
|
||||
id = String.valueOf(options.getId()); |
|
||||
break; |
|
||||
case USER: |
|
||||
builder.append("feed/user/"); |
|
||||
id = String.valueOf(options.getId()); |
|
||||
break; |
|
||||
case HIGHLIGHT: |
|
||||
case STORY_ARCHIVE: |
|
||||
builder.append("feed/reels_media/?user_ids="); |
|
||||
id = options.getName(); |
|
||||
break; |
|
||||
case STORY: |
|
||||
break; |
|
||||
// case FEED_STORY_POSITION: |
|
||||
// break; |
|
||||
} |
|
||||
if (id == null) { |
|
||||
return null; |
|
||||
} |
|
||||
builder.append(id); |
|
||||
if (type != StoryViewerOptions.Type.HIGHLIGHT && type != StoryViewerOptions.Type.STORY_ARCHIVE) { |
|
||||
builder.append("/story/"); |
|
||||
} |
|
||||
return builder.toString(); |
|
||||
} |
|
||||
|
|
||||
private List<FeedStoryModel> sort(final List<FeedStoryModel> list) { |
|
||||
final List<FeedStoryModel> listCopy = new ArrayList<>(list); |
|
||||
Collections.sort(listCopy, (o1, o2) -> { |
|
||||
int result; |
|
||||
switch (Utils.settingsHelper.getString(PreferenceKeys.STORY_SORT)) { |
|
||||
case "1": |
|
||||
result = Long.compare(o2.getTimestamp(), o1.getTimestamp()); |
|
||||
break; |
|
||||
case "2": |
|
||||
result = Long.compare(o1.getTimestamp(), o2.getTimestamp()); |
|
||||
break; |
|
||||
default: |
|
||||
result = 0; |
|
||||
} |
|
||||
return result; |
|
||||
}); |
|
||||
return listCopy; |
|
||||
} |
|
||||
|
|
||||
public static class ArchiveFetchResponse { |
|
||||
private final List<HighlightModel> archives; |
|
||||
private final boolean hasNextPage; |
|
||||
private final String nextCursor; |
|
||||
|
|
||||
public ArchiveFetchResponse(final List<HighlightModel> archives, final boolean hasNextPage, final String nextCursor) { |
|
||||
this.archives = archives; |
|
||||
this.hasNextPage = hasNextPage; |
|
||||
this.nextCursor = nextCursor; |
|
||||
} |
|
||||
|
|
||||
public List<HighlightModel> getResult() { |
|
||||
return archives; |
|
||||
} |
|
||||
|
|
||||
public boolean hasNextPage() { |
|
||||
return hasNextPage; |
|
||||
} |
|
||||
|
|
||||
public String getNextCursor() { |
|
||||
return nextCursor; |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,309 @@ |
|||||
|
package awais.instagrabber.webservices |
||||
|
|
||||
|
import android.util.Log |
||||
|
import awais.instagrabber.fragments.settings.PreferenceKeys |
||||
|
import awais.instagrabber.models.FeedStoryModel |
||||
|
import awais.instagrabber.models.HighlightModel |
||||
|
import awais.instagrabber.models.StoryModel |
||||
|
import awais.instagrabber.repositories.StoriesRepository |
||||
|
import awais.instagrabber.repositories.requests.StoryViewerOptions |
||||
|
import awais.instagrabber.repositories.responses.StoryStickerResponse |
||||
|
import awais.instagrabber.repositories.responses.User |
||||
|
import awais.instagrabber.utils.Constants |
||||
|
import awais.instagrabber.utils.ResponseBodyUtils |
||||
|
import awais.instagrabber.utils.TextUtils.isEmpty |
||||
|
import awais.instagrabber.utils.Utils |
||||
|
import awais.instagrabber.utils.extensions.TAG |
||||
|
import awais.instagrabber.webservices.RetrofitFactory.retrofit |
||||
|
import org.json.JSONArray |
||||
|
import org.json.JSONObject |
||||
|
import java.util.* |
||||
|
|
||||
|
object StoriesService : BaseService() { |
||||
|
private val repository: StoriesRepository = retrofit.create(StoriesRepository::class.java) |
||||
|
|
||||
|
suspend fun fetch(mediaId: Long): StoryModel { |
||||
|
val response = repository.fetch(mediaId) |
||||
|
val itemJson = JSONObject(response).getJSONArray("items").getJSONObject(0) |
||||
|
return ResponseBodyUtils.parseStoryItem(itemJson, false, null) |
||||
|
} |
||||
|
|
||||
|
suspend fun getFeedStories(): List<FeedStoryModel> { |
||||
|
val response = repository.getFeedStories() |
||||
|
return parseStoriesBody(response) |
||||
|
} |
||||
|
|
||||
|
private fun parseStoriesBody(body: String): List<FeedStoryModel> { |
||||
|
val feedStoryModels: MutableList<FeedStoryModel> = ArrayList() |
||||
|
val feedStoriesReel = JSONObject(body).getJSONArray("tray") |
||||
|
for (i in 0 until feedStoriesReel.length()) { |
||||
|
val node = feedStoriesReel.getJSONObject(i) |
||||
|
if (node.optBoolean("hide_from_feed_unit") && Utils.settingsHelper.getBoolean(PreferenceKeys.HIDE_MUTED_REELS)) continue |
||||
|
val userJson = node.getJSONObject(if (node.has("user")) "user" else "owner") |
||||
|
try { |
||||
|
val user = User(userJson.getLong("pk"), |
||||
|
userJson.getString("username"), |
||||
|
userJson.optString("full_name"), |
||||
|
userJson.optBoolean("is_private"), |
||||
|
userJson.getString("profile_pic_url"), |
||||
|
userJson.optBoolean("is_verified") |
||||
|
) |
||||
|
val timestamp = node.getLong("latest_reel_media") |
||||
|
val fullyRead = !node.isNull("seen") && node.getLong("seen") == timestamp |
||||
|
val itemJson = if (node.has("items")) node.getJSONArray("items").optJSONObject(0) else null |
||||
|
var firstStoryModel: StoryModel? = null |
||||
|
if (itemJson != null) { |
||||
|
firstStoryModel = ResponseBodyUtils.parseStoryItem(itemJson, false, null) |
||||
|
} |
||||
|
feedStoryModels.add(FeedStoryModel( |
||||
|
node.getString("id"), |
||||
|
user, |
||||
|
fullyRead, |
||||
|
timestamp, |
||||
|
firstStoryModel, |
||||
|
node.getInt("media_count"), |
||||
|
false, |
||||
|
node.optBoolean("has_besties_media"))) |
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "parseStoriesBody: ", e) |
||||
|
} // to cover promotional reels with non-long user pk's |
||||
|
} |
||||
|
val broadcasts = JSONObject(body).getJSONArray("broadcasts") |
||||
|
for (i in 0 until broadcasts.length()) { |
||||
|
val node = broadcasts.getJSONObject(i) |
||||
|
val userJson = node.getJSONObject("broadcast_owner") |
||||
|
val user = User(userJson.getLong("pk"), |
||||
|
userJson.getString("username"), |
||||
|
userJson.optString("full_name"), |
||||
|
userJson.optBoolean("is_private"), |
||||
|
userJson.getString("profile_pic_url"), |
||||
|
userJson.optBoolean("is_verified") |
||||
|
) |
||||
|
feedStoryModels.add(FeedStoryModel( |
||||
|
node.getString("id"), |
||||
|
user, |
||||
|
false, |
||||
|
node.getLong("published_time"), |
||||
|
ResponseBodyUtils.parseBroadcastItem(node), |
||||
|
1, |
||||
|
isLive = true, |
||||
|
isBestie = false |
||||
|
)) |
||||
|
} |
||||
|
return sort(feedStoryModels) |
||||
|
} |
||||
|
|
||||
|
suspend fun fetchHighlights(profileId: Long): List<HighlightModel> { |
||||
|
val response = repository.fetchHighlights(profileId) |
||||
|
val highlightsReel = JSONObject(response).getJSONArray("tray") |
||||
|
val length = highlightsReel.length() |
||||
|
val highlightModels: MutableList<HighlightModel> = ArrayList() |
||||
|
for (i in 0 until length) { |
||||
|
val highlightNode = highlightsReel.getJSONObject(i) |
||||
|
highlightModels.add(HighlightModel( |
||||
|
highlightNode.getString("title"), |
||||
|
highlightNode.getString(Constants.EXTRAS_ID), |
||||
|
highlightNode.getJSONObject("cover_media") |
||||
|
.getJSONObject("cropped_image_version") |
||||
|
.getString("url"), |
||||
|
highlightNode.getLong("latest_reel_media"), |
||||
|
highlightNode.getInt("media_count") |
||||
|
)) |
||||
|
} |
||||
|
return highlightModels |
||||
|
} |
||||
|
|
||||
|
suspend fun fetchArchive(maxId: String): ArchiveFetchResponse { |
||||
|
val form = mutableMapOf( |
||||
|
"include_suggested_highlights" to "false", |
||||
|
"is_in_archive_home" to "true", |
||||
|
"include_cover" to "1", |
||||
|
) |
||||
|
if (!isEmpty(maxId)) { |
||||
|
form["max_id"] = maxId // NOT TESTED |
||||
|
} |
||||
|
val response = repository.fetchArchive(form) |
||||
|
val data = JSONObject(response) |
||||
|
val highlightsReel = data.getJSONArray("items") |
||||
|
val length = highlightsReel.length() |
||||
|
val highlightModels: MutableList<HighlightModel> = ArrayList() |
||||
|
for (i in 0 until length) { |
||||
|
val highlightNode = highlightsReel.getJSONObject(i) |
||||
|
highlightModels.add(HighlightModel( |
||||
|
null, |
||||
|
highlightNode.getString(Constants.EXTRAS_ID), |
||||
|
highlightNode.getJSONObject("cover_image_version").getString("url"), |
||||
|
highlightNode.getLong("latest_reel_media"), |
||||
|
highlightNode.getInt("media_count") |
||||
|
)) |
||||
|
} |
||||
|
return ArchiveFetchResponse(highlightModels, data.getBoolean("more_available"), data.getString("max_id")) |
||||
|
} |
||||
|
|
||||
|
suspend fun getUserStory(options: StoryViewerOptions): List<StoryModel> { |
||||
|
val url = buildUrl(options) ?: return emptyList() |
||||
|
val response = repository.getUserStory(url) |
||||
|
val isLocOrHashtag = options.type == StoryViewerOptions.Type.LOCATION || options.type == StoryViewerOptions.Type.HASHTAG |
||||
|
val isHighlight = options.type == StoryViewerOptions.Type.HIGHLIGHT || options.type == StoryViewerOptions.Type.STORY_ARCHIVE |
||||
|
var data: JSONObject? = JSONObject(response) |
||||
|
data = if (!isHighlight) { |
||||
|
data?.optJSONObject(if (isLocOrHashtag) "story" else "reel") |
||||
|
} else { |
||||
|
data?.getJSONObject("reels")?.optJSONObject(options.name) |
||||
|
} |
||||
|
var username: String? = null |
||||
|
if (data != null && !isLocOrHashtag) { |
||||
|
username = data.getJSONObject("user").getString("username") |
||||
|
} |
||||
|
val media: JSONArray? = data?.optJSONArray("items") |
||||
|
return if (media?.length() ?: 0 > 0 && media?.optJSONObject(0) != null) { |
||||
|
val mediaLen = media.length() |
||||
|
val models: MutableList<StoryModel> = ArrayList() |
||||
|
for (i in 0 until mediaLen) { |
||||
|
data = media.getJSONObject(i) |
||||
|
models.add(ResponseBodyUtils.parseStoryItem(data, isLocOrHashtag, username)) |
||||
|
} |
||||
|
models |
||||
|
} else emptyList() |
||||
|
} |
||||
|
|
||||
|
private suspend fun respondToSticker( |
||||
|
csrfToken: String, |
||||
|
userId: Long, |
||||
|
deviceUuid: String, |
||||
|
storyId: String, |
||||
|
stickerId: String, |
||||
|
action: String, |
||||
|
arg1: String, |
||||
|
arg2: String, |
||||
|
): StoryStickerResponse { |
||||
|
val form = mapOf( |
||||
|
"_csrftoken" to csrfToken, |
||||
|
"_uid" to userId, |
||||
|
"_uuid" to deviceUuid, |
||||
|
"mutation_token" to UUID.randomUUID().toString(), |
||||
|
"client_context" to UUID.randomUUID().toString(), |
||||
|
"radio_type" to "wifi-none", |
||||
|
arg1 to arg2, |
||||
|
) |
||||
|
val signedForm = Utils.sign(form) |
||||
|
return repository.respondToSticker(storyId, stickerId, action, signedForm) |
||||
|
} |
||||
|
|
||||
|
suspend fun respondToQuestion( |
||||
|
csrfToken: String, |
||||
|
userId: Long, |
||||
|
deviceUuid: String, |
||||
|
storyId: String, |
||||
|
stickerId: String, |
||||
|
answer: String, |
||||
|
): StoryStickerResponse = respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_question_response", "response", answer) |
||||
|
|
||||
|
suspend fun respondToQuiz( |
||||
|
csrfToken: String, |
||||
|
userId: Long, |
||||
|
deviceUuid: String, |
||||
|
storyId: String, |
||||
|
stickerId: String, |
||||
|
answer: Int, |
||||
|
): StoryStickerResponse { |
||||
|
return respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_quiz_answer", "answer", answer.toString()) |
||||
|
} |
||||
|
|
||||
|
suspend fun respondToPoll( |
||||
|
csrfToken: String, |
||||
|
userId: Long, |
||||
|
deviceUuid: String, |
||||
|
storyId: String, |
||||
|
stickerId: String, |
||||
|
answer: Int, |
||||
|
): StoryStickerResponse = respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_poll_vote", "vote", answer.toString()) |
||||
|
|
||||
|
suspend fun respondToSlider( |
||||
|
csrfToken: String, |
||||
|
userId: Long, |
||||
|
deviceUuid: String, |
||||
|
storyId: String, |
||||
|
stickerId: String, |
||||
|
answer: Double, |
||||
|
): StoryStickerResponse = respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_slider_vote", "vote", answer.toString()) |
||||
|
|
||||
|
suspend fun seen( |
||||
|
csrfToken: String, |
||||
|
userId: Long, |
||||
|
deviceUuid: String, |
||||
|
storyMediaId: String, |
||||
|
takenAt: Long, |
||||
|
seenAt: Long, |
||||
|
): String { |
||||
|
val reelsForm = mapOf(storyMediaId to listOf(takenAt.toString() + "_" + seenAt)) |
||||
|
val form = mutableMapOf( |
||||
|
"_csrftoken" to csrfToken, |
||||
|
"_uid" to userId, |
||||
|
"_uuid" to deviceUuid, |
||||
|
"container_module" to "feed_timeline", |
||||
|
"reels" to reelsForm, |
||||
|
) |
||||
|
val signedForm = Utils.sign(form) |
||||
|
val queryMap = mapOf( |
||||
|
"reel" to "1", |
||||
|
"live_vod" to "0", |
||||
|
) |
||||
|
return repository.seen(queryMap, signedForm) |
||||
|
} |
||||
|
|
||||
|
private fun buildUrl(options: StoryViewerOptions): String? { |
||||
|
val builder = StringBuilder() |
||||
|
builder.append("https://i.instagram.com/api/v1/") |
||||
|
val type = options.type |
||||
|
var id: String? = null |
||||
|
when (type) { |
||||
|
StoryViewerOptions.Type.HASHTAG -> { |
||||
|
builder.append("tags/") |
||||
|
id = options.name |
||||
|
} |
||||
|
StoryViewerOptions.Type.LOCATION -> { |
||||
|
builder.append("locations/") |
||||
|
id = options.id.toString() |
||||
|
} |
||||
|
StoryViewerOptions.Type.USER -> { |
||||
|
builder.append("feed/user/") |
||||
|
id = options.id.toString() |
||||
|
} |
||||
|
StoryViewerOptions.Type.HIGHLIGHT, StoryViewerOptions.Type.STORY_ARCHIVE -> { |
||||
|
builder.append("feed/reels_media/?user_ids=") |
||||
|
id = options.name |
||||
|
} |
||||
|
StoryViewerOptions.Type.STORY -> { |
||||
|
} |
||||
|
else -> { |
||||
|
} |
||||
|
} |
||||
|
if (id == null) { |
||||
|
return null |
||||
|
} |
||||
|
builder.append(id) |
||||
|
if (type != StoryViewerOptions.Type.HIGHLIGHT && type != StoryViewerOptions.Type.STORY_ARCHIVE) { |
||||
|
builder.append("/story/") |
||||
|
} |
||||
|
return builder.toString() |
||||
|
} |
||||
|
|
||||
|
private fun sort(list: List<FeedStoryModel>): List<FeedStoryModel> { |
||||
|
val listCopy = ArrayList(list) |
||||
|
listCopy.sortWith { o1, o2 -> |
||||
|
when (Utils.settingsHelper.getString(PreferenceKeys.STORY_SORT)) { |
||||
|
"1" -> return@sortWith o2.timestamp.compareTo(o1.timestamp) |
||||
|
"2" -> return@sortWith o1.timestamp.compareTo(o2.timestamp) |
||||
|
else -> return@sortWith 0 |
||||
|
} |
||||
|
} |
||||
|
return listCopy |
||||
|
} |
||||
|
|
||||
|
class ArchiveFetchResponse(val result: List<HighlightModel>, val hasNextPage: Boolean, val nextCursor: String) { |
||||
|
fun hasNextPage(): Boolean { |
||||
|
return hasNextPage |
||||
|
} |
||||
|
} |
||||
|
} |
@ -1,101 +0,0 @@ |
|||||
package awais.instagrabber.webservices; |
|
||||
|
|
||||
import androidx.annotation.NonNull; |
|
||||
|
|
||||
import java.util.TimeZone; |
|
||||
|
|
||||
import awais.instagrabber.repositories.UserRepository; |
|
||||
import awais.instagrabber.repositories.responses.FriendshipStatus; |
|
||||
import awais.instagrabber.repositories.responses.User; |
|
||||
import awais.instagrabber.repositories.responses.UserSearchResponse; |
|
||||
import awais.instagrabber.repositories.responses.WrappedUser; |
|
||||
import retrofit2.Call; |
|
||||
import retrofit2.Callback; |
|
||||
import retrofit2.Response; |
|
||||
|
|
||||
public class UserService extends BaseService { |
|
||||
private static final String TAG = UserService.class.getSimpleName(); |
|
||||
|
|
||||
private final UserRepository repository; |
|
||||
|
|
||||
private static UserService instance; |
|
||||
|
|
||||
private UserService() { |
|
||||
repository = RetrofitFactory.INSTANCE |
|
||||
.getRetrofit() |
|
||||
.create(UserRepository.class); |
|
||||
} |
|
||||
|
|
||||
public static UserService getInstance() { |
|
||||
if (instance == null) { |
|
||||
instance = new UserService(); |
|
||||
} |
|
||||
return instance; |
|
||||
} |
|
||||
|
|
||||
public void getUserInfo(final long uid, final ServiceCallback<User> callback) { |
|
||||
final Call<WrappedUser> request = repository.getUserInfo(uid); |
|
||||
request.enqueue(new Callback<WrappedUser>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<WrappedUser> call, @NonNull final Response<WrappedUser> response) { |
|
||||
final WrappedUser user = response.body(); |
|
||||
if (user == null) { |
|
||||
callback.onSuccess(null); |
|
||||
return; |
|
||||
} |
|
||||
callback.onSuccess(user.getUser()); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<WrappedUser> call, @NonNull final Throwable t) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public void getUsernameInfo(final String username, final ServiceCallback<User> callback) { |
|
||||
final Call<WrappedUser> request = repository.getUsernameInfo(username); |
|
||||
request.enqueue(new Callback<WrappedUser>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<WrappedUser> call, @NonNull final Response<WrappedUser> response) { |
|
||||
final WrappedUser user = response.body(); |
|
||||
if (user == null) { |
|
||||
callback.onFailure(null); |
|
||||
return; |
|
||||
} |
|
||||
callback.onSuccess(user.getUser()); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<WrappedUser> call, @NonNull final Throwable t) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public void getUserFriendship(final long uid, final ServiceCallback<FriendshipStatus> callback) { |
|
||||
final Call<FriendshipStatus> request = repository.getUserFriendship(uid); |
|
||||
request.enqueue(new Callback<FriendshipStatus>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<FriendshipStatus> call, @NonNull final Response<FriendshipStatus> response) { |
|
||||
final FriendshipStatus status = response.body(); |
|
||||
if (status == null) { |
|
||||
callback.onSuccess(null); |
|
||||
return; |
|
||||
} |
|
||||
callback.onSuccess(status); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<FriendshipStatus> call, @NonNull final Throwable t) { |
|
||||
callback.onFailure(t); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
public Call<UserSearchResponse> search(final String query) { |
|
||||
final float timezoneOffset = (float) TimeZone.getDefault().getRawOffset() / 1000; |
|
||||
return repository.search(timezoneOffset, query); |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,29 @@ |
|||||
|
package awais.instagrabber.webservices |
||||
|
|
||||
|
import awais.instagrabber.repositories.UserRepository |
||||
|
import awais.instagrabber.repositories.responses.FriendshipStatus |
||||
|
import awais.instagrabber.repositories.responses.User |
||||
|
import awais.instagrabber.repositories.responses.UserSearchResponse |
||||
|
import awais.instagrabber.webservices.RetrofitFactory.retrofit |
||||
|
import java.util.* |
||||
|
|
||||
|
object UserService : BaseService() { |
||||
|
private val repository: UserRepository = retrofit.create(UserRepository::class.java) |
||||
|
|
||||
|
suspend fun getUserInfo(uid: Long): User { |
||||
|
val response = repository.getUserInfo(uid) |
||||
|
return response.user |
||||
|
} |
||||
|
|
||||
|
suspend fun getUsernameInfo(username: String): User { |
||||
|
val response = repository.getUsernameInfo(username) |
||||
|
return response.user |
||||
|
} |
||||
|
|
||||
|
suspend fun getUserFriendship(uid: Long): FriendshipStatus = repository.getUserFriendship(uid) |
||||
|
|
||||
|
suspend fun search(query: String): UserSearchResponse { |
||||
|
val timezoneOffset = TimeZone.getDefault().rawOffset.toFloat() / 1000 |
||||
|
return repository.search(timezoneOffset, query) |
||||
|
} |
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
package awais.instagrabber.viewmodels |
||||
|
|
||||
|
import androidx.lifecycle.SavedStateHandle |
||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4 |
||||
|
import org.junit.Test |
||||
|
import org.junit.runner.RunWith |
||||
|
|
||||
|
@RunWith(AndroidJUnit4::class) |
||||
|
internal class ProfileFragmentViewModelTest { |
||||
|
@Test |
||||
|
fun testNoUsernameNoCurrentUser() { |
||||
|
val state = SavedStateHandle(mutableMapOf<String, Any>( |
||||
|
"username" to "" |
||||
|
)) |
||||
|
val viewModel = ProfileFragmentViewModel(state) |
||||
|
|
||||
|
} |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue