improved twitter replies #809

This commit is contained in:
Mariotaku Lee 2017-04-29 11:19:31 +08:00
parent fa6d658394
commit 31086e7718
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
15 changed files with 291 additions and 60 deletions

View File

@ -102,8 +102,6 @@ public class ParcelableActivity extends ParcelableStatus implements Parcelable {
public boolean has_following_source = true;
public transient UserKey[] after_filtered_source_keys;
public transient ParcelableLiteUser[] after_filtered_sources;
public ParcelableActivity() {
}

View File

@ -21,7 +21,11 @@ package org.mariotaku.microblog.library.twitter;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.twitter.model.FavoritedPopup;
import org.mariotaku.microblog.library.twitter.model.StatusPage;
import org.mariotaku.restfu.annotation.method.GET;
import org.mariotaku.restfu.annotation.param.Headers;
import org.mariotaku.restfu.annotation.param.KeyValue;
import org.mariotaku.restfu.annotation.param.Path;
import org.mariotaku.restfu.annotation.param.Query;
/**
@ -30,5 +34,14 @@ import org.mariotaku.restfu.annotation.param.Query;
public interface TwitterWeb {
@GET("/i/activity/favorited_popup")
@Headers(value = {@KeyValue(key = "Accept", value = "application/json")})
FavoritedPopup getFavoritedPopup(@Query("id") String statusId) throws MicroBlogException;
@GET("/{screen_name}/status/{id}")
@Headers({@KeyValue(key = "Accept", value = "application/json, text/javascript, */*; q=0.01"),
@KeyValue(key = "X-Overlay-Request", value = "true"),
@KeyValue(key = "X-Requested-With", value = "XMLHttpRequest"),
@KeyValue(key = "User-Agent", value = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36")})
StatusPage getStatusPage(@Path("screen_name") String screenName, @Path("id") String statusId)
throws MicroBlogException;
}

View File

@ -0,0 +1,36 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.microblog.library.twitter.model;
import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonObject;
/**
* Created by mariotaku on 2017/4/1.
*/
@JsonObject
public class StatusPage {
@JsonField(name = "page")
String page;
public String getPage() {
return page;
}
}

View File

@ -14,6 +14,7 @@ import android.webkit.URLUtil;
import org.mariotaku.microblog.library.MicroBlog;
import org.mariotaku.restfu.http.Endpoint;
import org.mariotaku.restfu.http.MultiValueMap;
import org.mariotaku.restfu.http.SimpleValueMap;
import org.mariotaku.restfu.oauth.OAuthEndpoint;
import org.mariotaku.restfu.oauth.OAuthToken;
@ -252,6 +253,6 @@ public class MicroBlogAPIFactory implements TwidereConstants {
public interface ExtraHeaders {
@NonNull
List<Pair<String, String>> get();
List<Pair<String, String>> get(MultiValueMap<String> headers);
}
}

View File

@ -23,9 +23,9 @@ import android.annotation.SuppressLint
import android.content.Context
import android.database.Cursor
import android.database.CursorIndexOutOfBoundsException
import android.support.v4.util.LongSparseArray
import android.support.v4.widget.Space
import android.support.v7.widget.RecyclerView
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -133,7 +133,7 @@ class ParcelableActivitiesAdapter(
private var filteredUserKeys: Array<UserKey>? = null
private val gapLoadingIds: MutableSet<ObjectId> = HashSet()
private val reuseActivity = ParcelableActivity()
private val filterIdCache = LongSparseArray<Array<UserKey>?>()
private val filterIdCache = SparseArray<Array<UserKey>?>()
init {
eventListener = EventListener(this)
@ -241,6 +241,8 @@ class ParcelableActivitiesAdapter(
}
ITEM_VIEW_TYPE_TITLE_SUMMARY -> {
val activity = getActivityInternal(position, raw = false, reuse = true)
activity.after_filtered_source_keys = getAfterFilteredSourceKeys(position, false,
activity.source_keys)
(holder as ActivityTitleSummaryViewHolder).displayActivity(activity)
}
ITEM_VIEW_TYPE_STUB -> {
@ -274,12 +276,10 @@ class ParcelableActivitiesAdapter(
Activity.Action.FAVORITED_MEDIA_TAGGED, Activity.Action.JOINED_TWITTER -> {
if (mentionsOnly) return ITEM_VIEW_TYPE_EMPTY
filteredUserKeys?.let {
// ParcelableActivityUtils.initAfterFilteredSourceIds(activity, it, followingOnly)
// filterIdCache[activity._id] = activity.after_filtered_source_ids
// val sourceIds = activity.after_filtered_source_keys
// if (sourceIds != null && sourceIds.isEmpty()) {
// return ITEM_VIEW_TYPE_EMPTY
// }
val afterFiltered = getAfterFilteredSourceKeys(position, false)
if (afterFiltered != null && afterFiltered.isEmpty()) {
return ITEM_VIEW_TYPE_EMPTY
}
}
return ITEM_VIEW_TYPE_TITLE_SUMMARY
}
@ -347,6 +347,15 @@ class ParcelableActivitiesAdapter(
}, defValue = -1L, raw = raw)
}
fun getSourceKeys(adapterPosition: Int, raw: Boolean = false): Array<UserKey>? {
return getFieldValue(adapterPosition, readCursorValueAction = { cursor, indices ->
cursor.getString(indices[Activities.SOURCE_KEYS])?.let(UserKey::arrayOf)
}, readStatusValueAction = { activity ->
activity.source_keys
}, defValue = null, raw = raw)
}
fun getData(): List<ParcelableActivity>? {
return data
}
@ -356,7 +365,6 @@ class ParcelableActivitiesAdapter(
itemCounts[1] = if (ILoadMoreSupportAdapter.END in loadMoreIndicatorPosition) 1 else 0
}
private fun getActivityInternal(position: Int, raw: Boolean, reuse: Boolean): ParcelableActivity {
val dataPosition = position - activityStartIndex
val activityCount = getActivityCount(raw)
@ -375,6 +383,19 @@ class ParcelableActivitiesAdapter(
}
}
private fun getAfterFilteredSourceKeys(position: Int, raw: Boolean,
sourceKeys: Array<UserKey>? = getSourceKeys(position, raw)): Array<UserKey>? {
return filterIdCache[position] ?: run {
val allFiltered = filteredUserKeys
val keys = if (allFiltered != null) {
sourceKeys?.filterNot { it in allFiltered }?.toTypedArray()
} else {
sourceKeys
}
filterIdCache.put(position, keys)
return@run keys
}
}
private inline fun <T> getFieldValue(position: Int,
readCursorValueAction: (cursor: Cursor, indices: ObjectCursor.CursorIndices<ParcelableActivity>) -> T,

View File

@ -0,0 +1,58 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.extension.atto
import org.attoparser.dom.Document
import org.attoparser.dom.Element
/**
* Created by mariotaku on 2017/4/29.
*/
fun Document.firstElementOrNull(match: (Element) -> Boolean): Element? {
getChildrenOfType(Element::class.java).forEach { child ->
val element = child.firstElementOrNull(match)
if (element != null) return element
}
return null
}
fun Element.firstElementOrNull(match: (Element) -> Boolean): Element? {
if (match(this)) return this
getChildrenOfType(Element::class.java).forEach { child ->
val element = child.firstElementOrNull(match)
if (element != null) return element
}
return null
}
fun Element.filter(match: (Element) -> Boolean): List<Element> {
return filterTo(ArrayList(), match)
}
fun <C : MutableCollection<Element>> Element.filterTo(to: C, match: (Element) -> Boolean): C {
if (match(this)) {
to.add(this)
}
getChildrenOfType(Element::class.java).forEach { child ->
child.filterTo(to, match)
}
return to
}

View File

@ -0,0 +1,30 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.extension.restfu
import org.mariotaku.restfu.http.MultiValueMap
/**
* Created by mariotaku on 2017/4/29.
*/
operator fun <T> MultiValueMap<T>.contains(key: String): Boolean {
return getFirst(key) != null
}

View File

@ -84,6 +84,7 @@ import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter.IndicatorPosition
import org.mariotaku.twidere.adapter.iface.IStatusesAdapter
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.annotation.ProfileImageSize
import org.mariotaku.twidere.annotation.Referral
import org.mariotaku.twidere.constant.*
import org.mariotaku.twidere.constant.KeyboardShortcutConstants.*
@ -187,7 +188,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
}
adapter.loadMoreSupportedPosition = supportedPositions
setConversation(data)
val canLoadAllReplies = loader.canLoadAllReplies()
val canLoadAllReplies = loader.canLoadAllReplies
if (canLoadAllReplies) {
adapter.setReplyError(null)
} else {
@ -884,8 +885,8 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
nameView.updateText(formatter)
adapter.requestManager.loadProfileImage(context, status, adapter.profileImageStyle,
itemView.profileImage.cornerRadius, itemView.profileImage.cornerRadiusRatio)
.into(itemView.profileImage)
itemView.profileImage.cornerRadius, itemView.profileImage.cornerRadiusRatio,
size = ProfileImageSize.ORIGINAL).into(itemView.profileImage)
val typeIconRes = Utils.getUserTypeIconRes(status.user_is_verified, status.user_is_protected)
val typeDescriptionRes = Utils.getUserTypeDescriptionRes(status.user_is_verified, status.user_is_protected)

View File

@ -22,16 +22,21 @@ package org.mariotaku.twidere.loader.statuses
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread
import org.attoparser.config.ParseConfiguration
import org.attoparser.dom.DOMMarkupParser
import org.mariotaku.commons.parcel.ParcelUtils
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.mastodon.Mastodon
import org.mariotaku.microblog.library.twitter.TwitterWeb
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.SearchQuery
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.twidere.alias.MastodonStatus
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.exception.APINotSupportedException
import org.mariotaku.twidere.extension.atto.filter
import org.mariotaku.twidere.extension.atto.firstElementOrNull
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.isOfficial
@ -40,9 +45,11 @@ import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.pagination.PaginatedArrayList
import org.mariotaku.twidere.model.pagination.PaginatedList
import org.mariotaku.twidere.model.pagination.Pagination
import org.mariotaku.twidere.model.pagination.SinceMaxPagination
import org.mariotaku.twidere.model.util.ParcelableStatusUtils
import org.mariotaku.twidere.util.InternalTwitterContentUtils
import java.text.ParseException
import java.util.*
class ConversationLoader(
@ -54,7 +61,8 @@ class ConversationLoader(
) : AbsRequestStatusesLoader(context, status.account_key, adapterData, null, -1, fromUser, loadingMore) {
private val status = ParcelUtils.clone(status)
private var canLoadAllReplies: Boolean = false
var canLoadAllReplies: Boolean = false
private set
init {
ParcelableStatusUtils.makeOriginalStatus(this.status)
@ -66,9 +74,7 @@ class ConversationLoader(
AccountType.MASTODON -> return getMastodonStatuses(account, paging).mapTo(PaginatedArrayList()) {
it.toParcelable(account)
}
else -> return getMicroBlogStatuses(account, paging).mapMicroBlogToPaginated {
it.toParcelable(account, profileImageSize)
}
else -> return getMicroBlogStatuses(account, paging)
}
}
@ -80,27 +86,33 @@ class ConversationLoader(
}
@Throws(MicroBlogException::class)
private fun getMicroBlogStatuses(account: AccountDetails, paging: Paging): List<Status> {
private fun getMicroBlogStatuses(account: AccountDetails, paging: Paging): PaginatedList<ParcelableStatus> {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
canLoadAllReplies = false
when (account.type) {
AccountType.TWITTER -> {
val isOfficial = account.isOfficial(context)
canLoadAllReplies = isOfficial
if (isOfficial) {
return microBlog.showConversation(status.id, paging)
}
// if (isOfficial) {
// return microBlog.showConversation(status.id, paging).mapMicroBlogToPaginated {
// it.toParcelable(account, profileImageSize)
// }
// }
return showConversationCompat(microBlog, account, status, true)
}
AccountType.STATUSNET -> {
canLoadAllReplies = true
status.extras?.statusnet_conversation_id?.let {
return microBlog.getStatusNetConversation(it, paging)
return microBlog.getStatusNetConversation(it, paging).mapMicroBlogToPaginated {
it.toParcelable(account, profileImageSize)
}
}
}
AccountType.FANFOU -> {
canLoadAllReplies = true
return microBlog.getContextTimeline(status.id, paging)
return microBlog.getContextTimeline(status.id, paging).mapMicroBlogToPaginated {
it.toParcelable(account, profileImageSize)
}
}
else -> {
throw APINotSupportedException(account.type)
@ -110,9 +122,14 @@ class ConversationLoader(
return showConversationCompat(microBlog, account, status, true)
}
@WorkerThread
override fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean {
return InternalTwitterContentUtils.isFiltered(database, status, false)
}
@Throws(MicroBlogException::class)
private fun showConversationCompat(twitter: MicroBlog, details: AccountDetails,
status: ParcelableStatus, loadReplies: Boolean): List<Status> {
status: ParcelableStatus, loadReplies: Boolean): PaginatedList<ParcelableStatus> {
val statuses = ArrayList<Status>()
val pagination = this.pagination as? SinceMaxPagination
val maxId = pagination?.maxId
@ -120,6 +137,9 @@ class ConversationLoader(
val maxSortId = pagination?.maxSortId ?: -1
val sinceSortId = pagination?.sinceSortId ?: -1
val noSinceMaxId = maxId == null && sinceId == null
var nextPagination: Pagination? = null
// Load conversations
if (maxId != null && maxSortId < status.sort_id || noSinceMaxId) {
var inReplyToId: String? = maxId ?: status.in_reply_to_status_id
@ -131,35 +151,69 @@ class ConversationLoader(
count++
}
}
if (loadReplies) {
if (loadReplies || noSinceMaxId || sinceId != null && sinceSortId > status.sort_id) {
// Load replies
if (sinceId != null && sinceSortId > status.sort_id || noSinceMaxId) {
val query = SearchQuery()
var repliesLoaded = false
try {
if (details.type == AccountType.TWITTER) {
query.query("to:${status.user_screen_name}")
if (noSinceMaxId) {
statuses.addAll(loadTwitterWebReplies(details, twitter))
}
repliesLoaded = true
}
} catch (e: MicroBlogException) {
// Ignore
}
if (!repliesLoaded) {
val query = SearchQuery()
query.count(100)
if (details.type == AccountType.TWITTER) {
query.query("to:${status.user_screen_name} since_id:${status.id}")
} else {
query.query("@${status.user_screen_name}")
}
query.sinceId(sinceId ?: status.id)
try {
twitter.search(query).filterTo(statuses) { it.inReplyToStatusId == status.id }
val queryResult = twitter.search(query)
val firstId = queryResult.firstOrNull()?.id
if (firstId != null) {
nextPagination = SinceMaxPagination.sinceId(firstId, 0)
}
queryResult.filterTo(statuses) { it.inReplyToStatusId == status.id }
} catch (e: MicroBlogException) {
// Ignore for now
}
}
}
return statuses
return statuses.mapTo(PaginatedArrayList()) {
it.toParcelable(details, profileImageSize)
}.apply {
this.nextPage = nextPagination
}
}
fun canLoadAllReplies(): Boolean {
return canLoadAllReplies
}
private fun loadTwitterWebReplies(details: AccountDetails, twitter: MicroBlog): List<Status> {
val web = details.newMicroBlogInstance(context, TwitterWeb::class.java)
val page = web.getStatusPage(status.user_screen_name, status.id).page
@WorkerThread
override fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean {
return InternalTwitterContentUtils.isFiltered(database, status, false)
}
val parser = DOMMarkupParser(ParseConfiguration.htmlConfiguration())
val statusIds = ArrayList<String>()
try {
val document = parser.parse(page)
val repliesElement = document.firstElementOrNull { element ->
element.getAttributeValue("data-component-context") == "replies"
} ?: throw MicroBlogException("No replies data found")
repliesElement.filter {
it.getAttributeValue("data-item-type") == "tweet" && it.hasAttribute("data-item-id")
}.mapTo(statusIds) { it.getAttributeValue("data-item-id") }
} catch (e: ParseException) {
throw MicroBlogException(e)
}
if (statusIds.isEmpty()) {
throw MicroBlogException("Invalid response")
}
return twitter.lookupStatuses(statusIds.distinct().toTypedArray())
}
}

View File

@ -45,15 +45,12 @@ object ParcelableActivityUtils {
}
fun getAfterFilteredSources(activity: ParcelableActivity): Array<ParcelableLiteUser> {
if (activity.after_filtered_sources != null) return activity.after_filtered_sources
if (activity.after_filtered_source_keys == null || activity.sources.size == activity.after_filtered_source_keys.size) {
return activity.sources_lite
}
val result = Array(activity.after_filtered_source_keys.size) { idx ->
return@Array activity.sources_lite.find { it.key == activity.after_filtered_source_keys[idx] }!!
}
activity.after_filtered_sources = result
return result
val sources = activity.sources_lite ?: return emptyArray()
val afterFilteredKeys = activity.after_filtered_source_keys?.takeIf {
it.size != sources.size
} ?: return sources
return sources.filter { it.key in afterFilteredKeys }.toTypedArray()
}

View File

@ -35,14 +35,18 @@ import org.mariotaku.ktextension.useCursor
import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.mastodon.Mastodon
import org.mariotaku.microblog.library.twitter.model.Activity
import org.mariotaku.sqliteqb.library.*
import org.mariotaku.sqliteqb.library.Columns.Column
import org.mariotaku.sqliteqb.library.query.SQLSelectQuery
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.constant.IntentConstants
import org.mariotaku.twidere.constant.databaseItemLimitKey
import org.mariotaku.twidere.extension.model.*
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.rawQuery
import org.mariotaku.twidere.model.*
@ -919,16 +923,24 @@ object DataStoreUtils {
@Throws(MicroBlogException::class)
fun findStatus(context: Context, accountKey: UserKey, statusId: String): ParcelableStatus {
val cached = findStatusInDatabases(context, accountKey, statusId)
val profileImageSize = context.getString(R.string.profile_image_size)
if (cached != null) return cached
val details = AccountUtils.getAccountDetails(AccountManager.get(context), accountKey,
true) ?: throw MicroBlogException("No account")
val microBlog = details.newMicroBlogInstance(context, MicroBlog::class.java)
val result = microBlog.showStatus(statusId)
val status = when (details.type) {
AccountType.MASTODON -> {
val mastodon = details.newMicroBlogInstance(context, Mastodon::class.java)
mastodon.fetchStatus(statusId).toParcelable(details)
}
else -> {
val microBlog = details.newMicroBlogInstance(context, MicroBlog::class.java)
microBlog.showStatus(statusId).toParcelable(details, profileImageSize)
}
}
val where = Expression.and(Expression.equalsArgs(Statuses.ACCOUNT_KEY),
Expression.equalsArgs(Statuses.ID)).sql
val whereArgs = arrayOf(accountKey.toString(), statusId)
val resolver = context.contentResolver
val status = result.toParcelable(details)
resolver.delete(CachedStatuses.CONTENT_URI, where, whereArgs)
resolver.insert(CachedStatuses.CONTENT_URI, ObjectCursor.valuesCreatorFrom(ParcelableStatus::class.java).create(status))
return status

View File

@ -49,8 +49,8 @@ class TwidereHttpRequestFactory(
headers.add("Authorization", RestFuUtils.sanitizeHeader(authorization.getHeader(endpoint, info)))
}
if (extraHeaders != null) {
for (pair in extraHeaders.get()) {
headers.add(pair.first, RestFuUtils.sanitizeHeader(pair.second))
for ((first, second) in extraHeaders.get(info.headers)) {
headers.add(first, RestFuUtils.sanitizeHeader(second))
}
}
return HttpRequest(restMethod, url, headers, info.getBody(converterFactory), null)

View File

@ -21,6 +21,8 @@ package org.mariotaku.twidere.util.api
import android.os.Build
import org.mariotaku.ktextension.bcp47Tag
import org.mariotaku.restfu.http.MultiValueMap
import org.mariotaku.twidere.extension.restfu.contains
import org.mariotaku.twidere.util.MicroBlogAPIFactory.ExtraHeaders
import java.util.*
@ -34,10 +36,12 @@ object TwitterAndroidExtraHeaders : ExtraHeaders {
const val apiVersion = "5"
const val internalVersionName = "7160062-r-930"
override fun get(): List<Pair<String, String>> {
override fun get(headers: MultiValueMap<String>): List<Pair<String, String>> {
val result = ArrayList<Pair<String, String>>()
val language = Locale.getDefault().bcp47Tag
result.add(Pair("User-Agent", userAgent))
if ("User-Agent" !in headers) {
result.add(Pair("User-Agent", userAgent))
}
result.add(Pair("Accept-Language", language))
result.add(Pair("X-Twitter-Client", clientName))
result.add(Pair("X-Twitter-Client-Language", language))

View File

@ -21,6 +21,8 @@ package org.mariotaku.twidere.util.api
import android.os.Build
import org.mariotaku.ktextension.bcp47Tag
import org.mariotaku.restfu.http.MultiValueMap
import org.mariotaku.twidere.extension.restfu.contains
import org.mariotaku.twidere.util.MicroBlogAPIFactory.ExtraHeaders
import java.util.*
@ -36,10 +38,12 @@ object TwitterMacExtraHeaders : ExtraHeaders {
const val platformArchitecture = "x86_64"
const val internalVersionName = "5002734"
override fun get(): List<Pair<String, String>> {
override fun get(headers: MultiValueMap<String>): List<Pair<String, String>> {
val result = ArrayList<Pair<String, String>>()
val language = Locale.getDefault().bcp47Tag
result.add(Pair("User-Agent", userAgent))
if ("User-Agent" !in headers) {
result.add(Pair("User-Agent", userAgent))
}
result.add(Pair("Accept-Language", language))
result.add(Pair("X-Twitter-Client", clientName))
result.add(Pair("X-Twitter-Client-Version", versionName))

View File

@ -19,6 +19,8 @@
package org.mariotaku.twidere.util.api
import org.mariotaku.restfu.http.MultiValueMap
import org.mariotaku.twidere.extension.restfu.contains
import org.mariotaku.twidere.util.MicroBlogAPIFactory
/**
@ -26,8 +28,8 @@ import org.mariotaku.twidere.util.MicroBlogAPIFactory
*/
class UserAgentExtraHeaders(val userAgent: String?) : MicroBlogAPIFactory.ExtraHeaders {
override fun get(): List<Pair<String, String>> {
if (userAgent == null) return emptyList()
override fun get(headers: MultiValueMap<String>): List<Pair<String, String>> {
if (userAgent == null || "User-Agent" in headers) return emptyList()
return listOf(Pair("User-Agent", userAgent))
}
}