~captainepoch/husky

6d8aa8ff4775b8f44e7e2919db7eec82fc5d9832 — Adolfo Santiago 5 months ago 237241b
Formatting, license header
M husky/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt => husky/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +265 -220
@@ 1,21 1,53 @@
/* Copyright 2017 Andrew Dawson
/*
 * Husky -- A Pleroma client for Android
 *
 * This file is a part of Tusky.
 * Copyright (C) 2021  The Husky Developers
 * Copyright (C) 2017  Alibek "a1batross" Omarov
 *
 * 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 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.
 *
 * Tusky 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.
 * 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 Tusky; if not,
 * see <http://www.gnu.org/licenses>. */
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.keylesspalace.tusky.network

import com.keylesspalace.tusky.entity.*
import com.keylesspalace.tusky.entity.AccessToken
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Announcement
import com.keylesspalace.tusky.entity.AppCredentials
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Chat
import com.keylesspalace.tusky.entity.ChatMessage
import com.keylesspalace.tusky.entity.Conversation
import com.keylesspalace.tusky.entity.DeletedStatus
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.EmojiReaction
import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.IdentityProof
import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.entity.Marker
import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.entity.NewChatMessage
import com.keylesspalace.tusky.entity.NewStatus
import com.keylesspalace.tusky.entity.NodeInfo
import com.keylesspalace.tusky.entity.NodeInfoLinks
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Relationship
import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.entity.SearchResult
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.StatusContext
import com.keylesspalace.tusky.entity.StickerPack
import io.reactivex.Completable
import io.reactivex.Single
import okhttp3.MultipartBody


@@ 23,8 55,21 @@ import okhttp3.RequestBody
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.*
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.HTTP
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Part
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.Url

/**
 * for documentation of the Mastodon REST API see https://docs.joinmastodon.org/api/


@@ 53,66 98,66 @@ interface MastodonApi {

    @GET("api/v1/timelines/home?with_muted=true")
    fun homeTimeline(
            @Query("max_id") maxId: String?,
            @Query("since_id") sinceId: String?,
            @Query("limit") limit: Int?
        @Query("max_id") maxId: String?,
        @Query("since_id") sinceId: String?,
        @Query("limit") limit: Int?
    ): Call<List<Status>>

    @GET("api/v1/timelines/home?with_muted=true")
    fun homeTimelineSingle(
            @Query("max_id") maxId: String?,
            @Query("since_id") sinceId: String?,
            @Query("limit") limit: Int?
        @Query("max_id") maxId: String?,
        @Query("since_id") sinceId: String?,
        @Query("limit") limit: Int?
    ): Single<List<Status>>

    @GET("api/v1/timelines/public?with_muted=true")
    fun publicTimeline(
            @Query("local") local: Boolean?,
            @Query("max_id") maxId: String?,
            @Query("since_id") sinceId: String?,
            @Query("limit") limit: Int?
        @Query("local") local: Boolean?,
        @Query("max_id") maxId: String?,
        @Query("since_id") sinceId: String?,
        @Query("limit") limit: Int?
    ): Call<List<Status>>

    @GET("api/v1/timelines/tag/{hashtag}?with_muted=true")
    fun hashtagTimeline(
            @Path("hashtag") hashtag: String,
            @Query("any[]") any: List<String>?,
            @Query("local") local: Boolean?,
            @Query("max_id") maxId: String?,
            @Query("since_id") sinceId: String?,
            @Query("limit") limit: Int?
        @Path("hashtag") hashtag: String,
        @Query("any[]") any: List<String>?,
        @Query("local") local: Boolean?,
        @Query("max_id") maxId: String?,
        @Query("since_id") sinceId: String?,
        @Query("limit") limit: Int?
    ): Call<List<Status>>

    @GET("api/v1/timelines/list/{listId}?with_muted=true")
    fun listTimeline(
            @Path("listId") listId: String,
            @Query("max_id") maxId: String?,
            @Query("since_id") sinceId: String?,
            @Query("limit") limit: Int?
        @Path("listId") listId: String,
        @Query("max_id") maxId: String?,
        @Query("since_id") sinceId: String?,
        @Query("limit") limit: Int?
    ): Call<List<Status>>

    @GET("api/v1/notifications")
    fun notifications(
            @Query("max_id") maxId: String?,
            @Query("since_id") sinceId: String?,
            @Query("limit") limit: Int?,
            @Query("exclude_types[]") excludes: Set<Notification.Type>?,
            @Query("with_muted") withMuted: Boolean?
        @Query("max_id") maxId: String?,
        @Query("since_id") sinceId: String?,
        @Query("limit") limit: Int?,
        @Query("exclude_types[]") excludes: Set<Notification.Type>?,
        @Query("with_muted") withMuted: Boolean?
    ): Call<List<Notification>>

    @GET("api/v1/markers")
    fun markersWithAuth(
            @Header("Authorization") auth: String,
            @Header(DOMAIN_HEADER) domain: String,
            @Query("timeline[]") timelines: List<String>
        @Header("Authorization") auth: String,
        @Header(DOMAIN_HEADER) domain: String,
        @Query("timeline[]") timelines: List<String>
    ): Single<Map<String, Marker>>

    @GET("api/v1/notifications?with_muted=true")
    fun notificationsWithAuth(
            @Header("Authorization") auth: String,
            @Header(DOMAIN_HEADER) domain: String,
            @Query("since_id") sinceId: String?,
            @Query("include_types[]") includeTypes: List<String>?
        @Header("Authorization") auth: String,
        @Header(DOMAIN_HEADER) domain: String,
        @Query("since_id") sinceId: String?,
        @Query("include_types[]") includeTypes: List<String>?
    ): Single<List<Notification>>

    @POST("api/v1/notifications/clear")


@@ 120,122 165,122 @@ interface MastodonApi {

    @GET("api/v1/notifications/{id}")
    fun notification(
            @Path("id") notificationId: String
        @Path("id") notificationId: String
    ): Call<Notification>

    @Multipart
    @POST("api/v1/media")
    fun uploadMedia(
            @Part file: MultipartBody.Part,
            @Part description: MultipartBody.Part? = null
        @Part file: MultipartBody.Part,
        @Part description: MultipartBody.Part? = null
    ): Single<Attachment>

    @FormUrlEncoded
    @PUT("api/v1/media/{mediaId}")
    fun updateMedia(
            @Path("mediaId") mediaId: String,
            @Field("description") description: String
        @Path("mediaId") mediaId: String,
        @Field("description") description: String
    ): Single<Attachment>

    @POST("api/v1/statuses")
    fun createStatus(
            @Header("Authorization") auth: String,
            @Header(DOMAIN_HEADER) domain: String,
            @Header("Idempotency-Key") idempotencyKey: String,
            @Body status: NewStatus
        @Header("Authorization") auth: String,
        @Header(DOMAIN_HEADER) domain: String,
        @Header("Idempotency-Key") idempotencyKey: String,
        @Body status: NewStatus
    ): Call<Status>

    @GET("api/v1/statuses/{id}")
    fun status(
            @Path("id") statusId: String
        @Path("id") statusId: String
    ): Call<Status>

    @GET("api/v1/statuses/{id}")
    fun statusSingle(
            @Path("id") statusId: String
        @Path("id") statusId: String
    ): Single<Status>

    @GET("api/v1/statuses/{id}/context")
    fun statusContext(
            @Path("id") statusId: String
        @Path("id") statusId: String
    ): Call<StatusContext>

    @GET("api/v1/statuses/{id}/reblogged_by")
    fun statusRebloggedBy(
            @Path("id") statusId: String,
            @Query("max_id") maxId: String?
        @Path("id") statusId: String,
        @Query("max_id") maxId: String?
    ): Single<Response<List<Account>>>

    @GET("api/v1/statuses/{id}/favourited_by")
    fun statusFavouritedBy(
            @Path("id") statusId: String,
            @Query("max_id") maxId: String?
        @Path("id") statusId: String,
        @Query("max_id") maxId: String?
    ): Single<Response<List<Account>>>

    @DELETE("api/v1/statuses/{id}")
    fun deleteStatus(
            @Path("id") statusId: String
        @Path("id") statusId: String
    ): Single<DeletedStatus>

    @POST("api/v1/statuses/{id}/reblog")
    fun reblogStatus(
            @Path("id") statusId: String
        @Path("id") statusId: String
    ): Single<Status>

    @POST("api/v1/statuses/{id}/unreblog")
    fun unreblogStatus(
            @Path("id") statusId: String
        @Path("id") statusId: String
    ): Single<Status>

    @POST("api/v1/statuses/{id}/favourite")
    fun favouriteStatus(
            @Path("id") statusId: String
        @Path("id") statusId: String
    ): Single<Status>

    @POST("api/v1/statuses/{id}/unfavourite")
    fun unfavouriteStatus(
            @Path("id") statusId: String
        @Path("id") statusId: String
    ): Single<Status>

    @POST("api/v1/statuses/{id}/bookmark")
    fun bookmarkStatus(
            @Path("id") statusId: String
        @Path("id") statusId: String
    ): Single<Status>

    @POST("api/v1/statuses/{id}/unbookmark")
    fun unbookmarkStatus(
            @Path("id") statusId: String
        @Path("id") statusId: String
    ): Single<Status>

    @POST("api/v1/statuses/{id}/pin")
    fun pinStatus(
            @Path("id") statusId: String
        @Path("id") statusId: String
    ): Single<Status>

    @POST("api/v1/statuses/{id}/unpin")
    fun unpinStatus(
            @Path("id") statusId: String
        @Path("id") statusId: String
    ): Single<Status>
    

    @POST("api/v1/statuses/{id}/mute")
    fun muteConversation(
            @Path("id") statusId: String
        @Path("id") statusId: String
    ): Single<Status>
    

    @POST("api/v1/statuses/{id}/unmute")
    fun unmuteConversation(
           @Path("id") statusId: String
        @Path("id") statusId: String
    ): Single<Status>
    

    @GET("api/v1/scheduled_statuses")
    fun scheduledStatuses(
            @Query("limit") limit: Int? = null,
            @Query("max_id") maxId: String? = null
        @Query("limit") limit: Int? = null,
        @Query("max_id") maxId: String? = null
    ): Single<List<ScheduledStatus>>

    @DELETE("api/v1/scheduled_statuses/{id}")
    fun deleteScheduledStatus(
            @Path("id") scheduledStatusId: String
        @Path("id") scheduledStatusId: String
    ): Single<ResponseBody>

    @GET("api/v1/accounts/verify_credentials")


@@ 244,39 289,39 @@ interface MastodonApi {
    @FormUrlEncoded
    @PATCH("api/v1/accounts/update_credentials")
    fun accountUpdateSource(
            @Field("source[privacy]") privacy: String?,
            @Field("source[sensitive]") sensitive: Boolean?
        @Field("source[privacy]") privacy: String?,
        @Field("source[sensitive]") sensitive: Boolean?
    ): Call<Account>

    @Multipart
    @PATCH("api/v1/accounts/update_credentials")
    fun accountUpdateCredentials(
            @Part(value = "display_name") displayName: RequestBody?,
            @Part(value = "note") note: RequestBody?,
            @Part(value = "locked") locked: RequestBody?,
            @Part avatar: MultipartBody.Part?,
            @Part header: MultipartBody.Part?,
            @Part(value = "fields_attributes[0][name]") fieldName0: RequestBody?,
            @Part(value = "fields_attributes[0][value]") fieldValue0: RequestBody?,
            @Part(value = "fields_attributes[1][name]") fieldName1: RequestBody?,
            @Part(value = "fields_attributes[1][value]") fieldValue1: RequestBody?,
            @Part(value = "fields_attributes[2][name]") fieldName2: RequestBody?,
            @Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?,
            @Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?,
            @Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody?
        @Part(value = "display_name") displayName: RequestBody?,
        @Part(value = "note") note: RequestBody?,
        @Part(value = "locked") locked: RequestBody?,
        @Part avatar: MultipartBody.Part?,
        @Part header: MultipartBody.Part?,
        @Part(value = "fields_attributes[0][name]") fieldName0: RequestBody?,
        @Part(value = "fields_attributes[0][value]") fieldValue0: RequestBody?,
        @Part(value = "fields_attributes[1][name]") fieldName1: RequestBody?,
        @Part(value = "fields_attributes[1][value]") fieldValue1: RequestBody?,
        @Part(value = "fields_attributes[2][name]") fieldName2: RequestBody?,
        @Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?,
        @Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?,
        @Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody?
    ): Call<Account>

    @GET("api/v1/accounts/search")
    fun searchAccounts(
            @Query("q") query: String,
            @Query("resolve") resolve: Boolean? = null,
            @Query("limit") limit: Int? = null,
            @Query("following") following: Boolean? = null
        @Query("q") query: String,
        @Query("resolve") resolve: Boolean? = null,
        @Query("limit") limit: Int? = null,
        @Query("following") following: Boolean? = null
    ): Single<List<Account>>

    @GET("api/v1/accounts/{id}")
    fun account(
            @Path("id") accountId: String
        @Path("id") accountId: String
    ): Single<Account>

    /**


@@ 290,78 335,78 @@ interface MastodonApi {
     */
    @GET("api/v1/accounts/{id}/statuses?with_muted=true")
    fun accountStatuses(
            @Path("id") accountId: String,
            @Query("max_id") maxId: String?,
            @Query("since_id") sinceId: String?,
            @Query("limit") limit: Int?,
            @Query("exclude_replies") excludeReplies: Boolean?,
            @Query("only_media") onlyMedia: Boolean?,
            @Query("pinned") pinned: Boolean?
        @Path("id") accountId: String,
        @Query("max_id") maxId: String?,
        @Query("since_id") sinceId: String?,
        @Query("limit") limit: Int?,
        @Query("exclude_replies") excludeReplies: Boolean?,
        @Query("only_media") onlyMedia: Boolean?,
        @Query("pinned") pinned: Boolean?
    ): Call<List<Status>>

    @GET("api/v1/accounts/{id}/followers")
    fun accountFollowers(
            @Path("id") accountId: String,
            @Query("max_id") maxId: String?
        @Path("id") accountId: String,
        @Query("max_id") maxId: String?
    ): Single<Response<List<Account>>>

    @GET("api/v1/accounts/{id}/following")
    fun accountFollowing(
            @Path("id") accountId: String,
            @Query("max_id") maxId: String?
        @Path("id") accountId: String,
        @Query("max_id") maxId: String?
    ): Single<Response<List<Account>>>

    @FormUrlEncoded
    @POST("api/v1/accounts/{id}/follow")
    fun followAccount(
            @Path("id") accountId: String,
            @Field("reblogs") showReblogs: Boolean? = null,
            @Field("notify") notify: Boolean? = null
        @Path("id") accountId: String,
        @Field("reblogs") showReblogs: Boolean? = null,
        @Field("notify") notify: Boolean? = null
    ): Single<Relationship>

    @POST("api/v1/accounts/{id}/unfollow")
    fun unfollowAccount(
            @Path("id") accountId: String
        @Path("id") accountId: String
    ): Single<Relationship>

    @POST("api/v1/accounts/{id}/block")
    fun blockAccount(
            @Path("id") accountId: String
        @Path("id") accountId: String
    ): Single<Relationship>

    @POST("api/v1/accounts/{id}/unblock")
    fun unblockAccount(
            @Path("id") accountId: String
        @Path("id") accountId: String
    ): Single<Relationship>

    @FormUrlEncoded
    @POST("api/v1/accounts/{id}/mute")
    fun muteAccount(
            @Path("id") accountId: String,
            @Field("notifications") notifications: Boolean? = null,
            @Field("duration") duration: Int? = null
        @Path("id") accountId: String,
        @Field("notifications") notifications: Boolean? = null,
        @Field("duration") duration: Int? = null
    ): Single<Relationship>

    @POST("api/v1/accounts/{id}/unmute")
    fun unmuteAccount(
            @Path("id") accountId: String
        @Path("id") accountId: String
    ): Single<Relationship>

    @GET("api/v1/accounts/relationships")
    fun relationships(
            @Query("id[]") accountIds: List<String>
        @Query("id[]") accountIds: List<String>
    ): Single<List<Relationship>>

    @GET("api/v1/accounts/{id}/identity_proofs")
    fun identityProofs(
            @Path("id") accountId: String
        @Path("id") accountId: String
    ): Single<List<IdentityProof>>
    

    @POST("api/v1/pleroma/accounts/{id}/subscribe")
    fun subscribeAccount(
        @Path("id") accountId: String
    ): Single<Relationship>
    

    @POST("api/v1/pleroma/accounts/{id}/unsubscribe")
    fun unsubscribeAccount(
        @Path("id") accountId: String


@@ 369,25 414,25 @@ interface MastodonApi {

    @GET("api/v1/blocks")
    fun blocks(
            @Query("max_id") maxId: String?
        @Query("max_id") maxId: String?
    ): Single<Response<List<Account>>>

    @GET("api/v1/mutes")
    fun mutes(
            @Query("max_id") maxId: String?
        @Query("max_id") maxId: String?
    ): Single<Response<List<Account>>>

    @GET("api/v1/domain_blocks")
    fun domainBlocks(
            @Query("max_id") maxId: String? = null,
            @Query("since_id") sinceId: String? = null,
            @Query("limit") limit: Int? = null
        @Query("max_id") maxId: String? = null,
        @Query("since_id") sinceId: String? = null,
        @Query("limit") limit: Int? = null
    ): Single<Response<List<String>>>

    @FormUrlEncoded
    @POST("api/v1/domain_blocks")
    fun blockDomain(
            @Field("domain") domain: String
        @Field("domain") domain: String
    ): Call<Any>

    @FormUrlEncoded


@@ 397,107 442,107 @@ interface MastodonApi {

    @GET("api/v1/favourites?with_muted=true")
    fun favourites(
            @Query("max_id") maxId: String?,
            @Query("since_id") sinceId: String?,
            @Query("limit") limit: Int?
        @Query("max_id") maxId: String?,
        @Query("since_id") sinceId: String?,
        @Query("limit") limit: Int?
    ): Call<List<Status>>

    @GET("api/v1/bookmarks?with_muted=true")
    fun bookmarks(
            @Query("max_id") maxId: String?,
            @Query("since_id") sinceId: String?,
            @Query("limit") limit: Int?
        @Query("max_id") maxId: String?,
        @Query("since_id") sinceId: String?,
        @Query("limit") limit: Int?
    ): Call<List<Status>>

    @GET("api/v1/follow_requests")
    fun followRequests(
            @Query("max_id") maxId: String?
        @Query("max_id") maxId: String?
    ): Single<Response<List<Account>>>

    @POST("api/v1/follow_requests/{id}/authorize")
    fun authorizeFollowRequest(
            @Path("id") accountId: String
        @Path("id") accountId: String
    ): Call<Relationship>

    @POST("api/v1/follow_requests/{id}/reject")
    fun rejectFollowRequest(
            @Path("id") accountId: String
        @Path("id") accountId: String
    ): Call<Relationship>

    @POST("api/v1/follow_requests/{id}/authorize")
    fun authorizeFollowRequestObservable(
            @Path("id") accountId: String
        @Path("id") accountId: String
    ): Single<Relationship>

    @POST("api/v1/follow_requests/{id}/reject")
    fun rejectFollowRequestObservable(
            @Path("id") accountId: String
        @Path("id") accountId: String
    ): Single<Relationship>

    @FormUrlEncoded
    @POST("api/v1/apps")
    fun authenticateApp(
            @Header(DOMAIN_HEADER) domain: String,
            @Field("client_name") clientName: String,
            @Field("redirect_uris") redirectUris: String,
            @Field("scopes") scopes: String,
            @Field("website") website: String
        @Header(DOMAIN_HEADER) domain: String,
        @Field("client_name") clientName: String,
        @Field("redirect_uris") redirectUris: String,
        @Field("scopes") scopes: String,
        @Field("website") website: String
    ): Call<AppCredentials>

    @FormUrlEncoded
    @POST("oauth/token")
    fun fetchOAuthToken(
            @Header(DOMAIN_HEADER) domain: String,
            @Field("client_id") clientId: String,
            @Field("client_secret") clientSecret: String,
            @Field("redirect_uri") redirectUri: String,
            @Field("code") code: String,
            @Field("grant_type") grantType: String
        @Header(DOMAIN_HEADER) domain: String,
        @Field("client_id") clientId: String,
        @Field("client_secret") clientSecret: String,
        @Field("redirect_uri") redirectUri: String,
        @Field("code") code: String,
        @Field("grant_type") grantType: String
    ): Call<AccessToken>

    @FormUrlEncoded
    @POST("api/v1/lists")
    fun createList(
            @Field("title") title: String
        @Field("title") title: String
    ): Single<MastoList>

    @FormUrlEncoded
    @PUT("api/v1/lists/{listId}")
    fun updateList(
            @Path("listId") listId: String,
            @Field("title") title: String
        @Path("listId") listId: String,
        @Field("title") title: String
    ): Single<MastoList>

    @DELETE("api/v1/lists/{listId}")
    fun deleteList(
            @Path("listId") listId: String
        @Path("listId") listId: String
    ): Completable

    @GET("api/v1/lists/{listId}/accounts")
    fun getAccountsInList(
            @Path("listId") listId: String,
            @Query("limit") limit: Int
        @Path("listId") listId: String,
        @Query("limit") limit: Int
    ): Single<List<Account>>

    @FormUrlEncoded
    // @DELETE doesn't support fields
    @HTTP(method = "DELETE", path = "api/v1/lists/{listId}/accounts", hasBody = true)
    fun deleteAccountFromList(
            @Path("listId") listId: String,
            @Field("account_ids[]") accountIds: List<String>
        @Path("listId") listId: String,
        @Field("account_ids[]") accountIds: List<String>
    ): Completable

    @FormUrlEncoded
    @POST("api/v1/lists/{listId}/accounts")
    fun addCountToList(
            @Path("listId") listId: String,
            @Field("account_ids[]") accountIds: List<String>
        @Path("listId") listId: String,
        @Field("account_ids[]") accountIds: List<String>
    ): Completable

    @GET("/api/v1/conversations")
    fun getConversations(
            @Query("max_id") maxId: String? = null,
            @Query("limit") limit: Int
        @Query("max_id") maxId: String? = null,
        @Query("limit") limit: Int
    ): Call<List<Conversation>>

    data class PostFilter(


@@ 513,83 558,83 @@ interface MastodonApi {

    @PUT("api/v1/filters/{id}")
    fun updateFilter(
            @Path("id") id: String,
            @Body body: PostFilter
        @Path("id") id: String,
        @Body body: PostFilter
    ): Call<Filter>

    @DELETE("api/v1/filters/{id}")
    fun deleteFilter(
            @Path("id") id: String
        @Path("id") id: String
    ): Call<ResponseBody>

    @FormUrlEncoded
    @POST("api/v1/polls/{id}/votes")
    fun voteInPoll(
            @Path("id") id: String,
            @Field("choices[]") choices: List<Int>
        @Path("id") id: String,
        @Field("choices[]") choices: List<Int>
    ): Single<Poll>

    @GET("api/v1/announcements")
    fun listAnnouncements(
            @Query("with_dismissed") withDismissed: Boolean = true
        @Query("with_dismissed") withDismissed: Boolean = true
    ): Single<List<Announcement>>

    @POST("api/v1/announcements/{id}/dismiss")
    fun dismissAnnouncement(
            @Path("id") announcementId: String
        @Path("id") announcementId: String
    ): Single<ResponseBody>

    @PUT("api/v1/announcements/{id}/reactions/{name}")
    fun addAnnouncementReaction(
            @Path("id") announcementId: String,
            @Path("name") name: String
        @Path("id") announcementId: String,
        @Path("name") name: String
    ): Single<ResponseBody>

    @DELETE("api/v1/announcements/{id}/reactions/{name}")
    fun removeAnnouncementReaction(
            @Path("id") announcementId: String,
            @Path("name") name: String
        @Path("id") announcementId: String,
        @Path("name") name: String
    ): Single<ResponseBody>

    @FormUrlEncoded
    @POST("api/v1/reports")
    fun reportObservable(
            @Field("account_id") accountId: String,
            @Field("status_ids[]") statusIds: List<String>,
            @Field("comment") comment: String,
            @Field("forward") isNotifyRemote: Boolean?
        @Field("account_id") accountId: String,
        @Field("status_ids[]") statusIds: List<String>,
        @Field("comment") comment: String,
        @Field("forward") isNotifyRemote: Boolean?
    ): Single<ResponseBody>

    @GET("api/v1/accounts/{id}/statuses?with_muted=true")
    fun accountStatusesObservable(
            @Path("id") accountId: String,
            @Query("max_id") maxId: String?,
            @Query("since_id") sinceId: String?,
            @Query("limit") limit: Int?,
            @Query("exclude_reblogs") excludeReblogs: Boolean?
        @Path("id") accountId: String,
        @Query("max_id") maxId: String?,
        @Query("since_id") sinceId: String?,
        @Query("limit") limit: Int?,
        @Query("exclude_reblogs") excludeReblogs: Boolean?
    ): Single<List<Status>>

    @GET("api/v1/statuses/{id}")
    fun statusObservable(
            @Path("id") statusId: String
        @Path("id") statusId: String
    ): Single<Status>

    @GET("api/v2/search")
    fun searchObservable(
            @Query("q") query: String?,
            @Query("type") type: String? = null,
            @Query("resolve") resolve: Boolean? = null,
            @Query("limit") limit: Int? = null,
            @Query("offset") offset: Int? = null,
            @Query("following") following: Boolean? = null
        @Query("q") query: String?,
        @Query("type") type: String? = null,
        @Query("resolve") resolve: Boolean? = null,
        @Query("limit") limit: Int? = null,
        @Query("offset") offset: Int? = null,
        @Query("following") following: Boolean? = null
    ): Single<SearchResult>
    

    @GET(".well-known/nodeinfo")
    fun getNodeinfoLinks() : Single<NodeInfoLinks>
    
    fun getNodeinfoLinks(): Single<NodeInfoLinks>

    @GET
    fun getNodeinfo(@Url url: String) : Single<NodeInfo>
    
    fun getNodeinfo(@Url url: String): Single<NodeInfo>

    @PUT("api/v1/pleroma/statuses/{id}/reactions/{emoji}")
    fun reactWithEmoji(
        @Path("id") statusId: String,


@@ 611,7 656,7 @@ interface MastodonApi {
    // NOT AN API CALLS NOT AN API CALLS NOT AN API CALLS NOT AN API CALLS
    // just for testing and because puniko asked me
    @GET("static/stickers.json")
    fun getStickers() : Single<Map<String, String>>
    fun getStickers(): Single<Map<String, String>>

    @GET
    fun getStickerPack(


@@ 621,64 666,64 @@ interface MastodonApi {

    @POST("api/v1/pleroma/chats/{id}/messages/{message_id}/read")
    fun markChatMessageAsRead(
            @Path("id") chatId: String,
            @Path("message_id") messageId: String
        @Path("id") chatId: String,
        @Path("message_id") messageId: String
    ): Single<ChatMessage>

    @DELETE("api/v1/pleroma/chats/{id}/messages/{message_id}")
    fun deleteChatMessage(
            @Path("id") chatId: String,
            @Path("message_id") messageId: String
        @Path("id") chatId: String,
        @Path("message_id") messageId: String
    ): Single<ChatMessage>

    @GET("api/v2/pleroma/chats")
    fun getChats(
            @Query("max_id") maxId: String?,
            @Query("min_id") minId: String?,
            @Query("since_id") sinceId: String?,
            @Query("offset") offset: Int?,
            @Query("limit") limit: Int?
        @Query("max_id") maxId: String?,
        @Query("min_id") minId: String?,
        @Query("since_id") sinceId: String?,
        @Query("offset") offset: Int?,
        @Query("limit") limit: Int?
    ): Single<List<Chat>>

    @GET("api/v1/pleroma/chats/{id}/messages")
    fun getChatMessages(
            @Path("id") chatId: String,
            @Query("max_id") maxId: String?,
            @Query("min_id") minId: String?,
            @Query("since_id") sinceId: String?,
            @Query("offset") offset: Int?,
            @Query("limit") limit: Int?
        @Path("id") chatId: String,
        @Query("max_id") maxId: String?,
        @Query("min_id") minId: String?,
        @Query("since_id") sinceId: String?,
        @Query("offset") offset: Int?,
        @Query("limit") limit: Int?
    ): Single<List<ChatMessage>>

    @POST("api/v1/pleroma/chats/{id}/messages")
    fun createChatMessage(
            @Header("Authorization") auth: String,
            @Header(DOMAIN_HEADER) domain: String,
            @Path("id") chatId: String,
            @Body chatMessage: NewChatMessage
        @Header("Authorization") auth: String,
        @Header(DOMAIN_HEADER) domain: String,
        @Path("id") chatId: String,
        @Body chatMessage: NewChatMessage
    ): Call<ChatMessage>

    @FormUrlEncoded
    @POST("api/v1/pleroma/chats/{id}/read")
    fun markChatAsRead(
            @Path("id") chatId: String,
            @Field("last_read_id") lastReadId: String? = null
        @Path("id") chatId: String,
        @Field("last_read_id") lastReadId: String? = null
    ): Single<Chat>

    @POST("api/v1/pleroma/chats/by-account-id/{id}")
    fun createChat(
            @Path("id") accountId: String
        @Path("id") accountId: String
    ): Single<Chat>

    @GET("api/v1/pleroma/chats/{id}")
    fun getChat(
            @Path("id") chatId: String
        @Path("id") chatId: String
    ): Single<Chat>

    @FormUrlEncoded
    @POST("api/v1/accounts/{id}/note")
    fun updateAccountNote(
            @Path("id") accountId: String,
            @Field("comment") note: String
        @Path("id") accountId: String,
        @Field("comment") note: String
    ): Single<Relationship>
}

M husky/app/src/main/java/com/keylesspalace/tusky/repository/ChatRepository.kt => husky/app/src/main/java/com/keylesspalace/tusky/repository/ChatRepository.kt +162 -97
@@ 1,3 1,23 @@
/*
 * Husky -- A Pleroma client for Android
 *
 * Copyright (C) 2021  The Husky Developers
 * Copyright (C) 2021  Andrew Dawson
 *
 * 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 <https://www.gnu.org/licenses/>.
 */

package com.keylesspalace.tusky.repository

import android.text.SpannedString


@@ 5,8 25,16 @@ import androidx.core.text.parseAsHtml
import androidx.core.text.toHtml
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.keylesspalace.tusky.db.*
import com.keylesspalace.tusky.entity.*
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.ChatEntity
import com.keylesspalace.tusky.db.ChatEntityWithAccount
import com.keylesspalace.tusky.db.ChatMessageEntity
import com.keylesspalace.tusky.db.ChatsDao
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Chat
import com.keylesspalace.tusky.entity.ChatMessage
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.repository.TimelineRequestMode.DISK
import com.keylesspalace.tusky.repository.TimelineRequestMode.NETWORK


@@ 17,39 45,56 @@ import com.keylesspalace.tusky.util.trimTrailingWhitespace
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import java.io.IOException
import java.util.*
import java.util.Date

typealias ChatStatus = Either<Placeholder, Chat>
typealias ChatMesssageOrPlaceholder = Either<Placeholder, ChatMessage>

interface ChatRepository {
    fun getChats(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int,
                    requestMode: TimelineRequestMode): Single<out List<ChatStatus>>

    fun getChatMessages(chatId: String, maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, requestMode: TimelineRequestMode) : Single<out List<ChatMesssageOrPlaceholder>>
    fun getChats(
        maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int,
        requestMode: TimelineRequestMode
    ): Single<out List<ChatStatus>>

    fun getChatMessages(
        chatId: String,
        maxId: String?,
        sinceId: String?,
        sincedIdMinusOne: String?,
        limit: Int,
        requestMode: TimelineRequestMode
    ): Single<out List<ChatMesssageOrPlaceholder>>
}

class ChatRepositoryImpl(
        private val chatsDao: ChatsDao,
        private val mastodonApi: MastodonApi,
        private val accountManager: AccountManager,
        private val gson: Gson
    private val chatsDao: ChatsDao,
    private val mastodonApi: MastodonApi,
    private val accountManager: AccountManager,
    private val gson: Gson
) : ChatRepository {

    override fun getChats(maxId: String?, sinceId: String?, sincedIdMinusOne: String?,
                          limit: Int, requestMode: TimelineRequestMode
    override fun getChats(
        maxId: String?, sinceId: String?, sincedIdMinusOne: String?,
        limit: Int, requestMode: TimelineRequestMode
    ): Single<out List<ChatStatus>> {
        val acc = accountManager.activeAccount ?: throw IllegalStateException()
        val accountId = acc.id

        return if (requestMode == DISK) {
        return if(requestMode == DISK) {
            this.getChatsFromDb(accountId, maxId, sinceId, limit)
        } else {
            getChatsFromNetwork(maxId, sinceId, sincedIdMinusOne, limit, accountId, requestMode)
        }
    }

    override fun getChatMessages(chatId: String, maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, requestMode: TimelineRequestMode) : Single<out List<ChatMesssageOrPlaceholder>> {
    override fun getChatMessages(
        chatId: String,
        maxId: String?,
        sinceId: String?,
        sincedIdMinusOne: String?,
        limit: Int,
        requestMode: TimelineRequestMode
    ): Single<out List<ChatMesssageOrPlaceholder>> {
        val acc = accountManager.activeAccount ?: throw IllegalStateException()
        val accountId = acc.id



@@ 62,9 107,10 @@ class ChatRepositoryImpl(
        return getChatMessagesFromNetwork(chatId, maxId, null, null, limit, accountId, requestMode)
    }

    private fun getChatsFromNetwork(maxId: String?, sinceId: String?,
                                       sinceIdMinusOne: String?, limit: Int,
                                       accountId: Long, requestMode: TimelineRequestMode
    private fun getChatsFromNetwork(
        maxId: String?, sinceId: String?,
        sinceIdMinusOne: String?, limit: Int,
        accountId: Long, requestMode: TimelineRequestMode
    ): Single<out List<ChatStatus>> {
        return mastodonApi.getChats(null, null, sinceIdMinusOne, 0, limit + 1)
            .map { chats ->


@@ 74,17 120,18 @@ class ChatRepositoryImpl(
                this.addFromDbIfNeeded(accountId, chats, maxId, sinceId, limit, requestMode)
            }
            .onErrorResumeNext { error ->
                if (error is IOException && requestMode != NETWORK) {
                if(error is IOException && requestMode != NETWORK) {
                    this.getChatsFromDb(accountId, maxId, sinceId, limit)
                } else {
                        Single.error(error)
                    }
                    Single.error(error)
                }
            }
    }

    private fun getChatMessagesFromNetwork(chatId: String, maxId: String?, sinceId: String?,
                                    sinceIdMinusOne: String?, limit: Int,
                                    accountId: Long, requestMode: TimelineRequestMode
    private fun getChatMessagesFromNetwork(
        chatId: String, maxId: String?, sinceId: String?,
        sinceIdMinusOne: String?, limit: Int,
        accountId: Long, requestMode: TimelineRequestMode
    ): Single<out List<ChatMesssageOrPlaceholder>> {
        return mastodonApi.getChatMessages(chatId, maxId, null, sinceIdMinusOne, 0, limit + 1).map {
            it.mapTo(mutableListOf(), ChatMessage::lift)


@@ 92,62 139,66 @@ class ChatRepositoryImpl(
    }


    private fun addFromDbIfNeeded(accountId: Long, chats: List<ChatStatus>,
                                  maxId: String?, sinceId: String?, limit: Int,
                                  requestMode: TimelineRequestMode
    private fun addFromDbIfNeeded(
        accountId: Long, chats: List<ChatStatus>,
        maxId: String?, sinceId: String?, limit: Int,
        requestMode: TimelineRequestMode
    ): Single<List<ChatStatus>> {
        return if (requestMode != NETWORK && chats.size < 2) {
            val newMaxID = if (chats.isEmpty()) {
        return if(requestMode != NETWORK && chats.size < 2) {
            val newMaxID = if(chats.isEmpty()) {
                maxId
            } else {
                chats.last { it.isRight() }.asRight().id
            }
            this.getChatsFromDb(accountId, newMaxID, sinceId, limit)
                    .map { fromDb ->
                        // If it's just placeholders and less than limit (so we exhausted both
                        // db and server at this point)
                        if (fromDb.size < limit && fromDb.all { !it.isRight() }) {
                            chats
                        } else {
                            chats + fromDb
                        }
                .map { fromDb ->
                    // If it's just placeholders and less than limit (so we exhausted both
                    // db and server at this point)
                    if(fromDb.size < limit && fromDb.all { !it.isRight() }) {
                        chats
                    } else {
                        chats + fromDb
                    }
                }
        } else {
            Single.just(chats)
        }
    }

    private fun getChatsFromDb(accountId: Long, maxId: String?, sinceId: String?,
                                  limit: Int): Single<out List<ChatStatus>> {
    private fun getChatsFromDb(
        accountId: Long, maxId: String?, sinceId: String?,
        limit: Int
    ): Single<out List<ChatStatus>> {
        return chatsDao.getChatsForAccount(accountId, maxId, sinceId, limit)
                .subscribeOn(Schedulers.io())
                .map { chats ->
                    chats.map { it.toChat(gson) }
                }
            .subscribeOn(Schedulers.io())
            .map { chats ->
                chats.map { it.toChat(gson) }
            }
    }


    private fun saveChatsToDb(accountId: Long, chats: List<Chat>,
                                 maxId: String?, sinceId: String?
    private fun saveChatsToDb(
        accountId: Long, chats: List<Chat>,
        maxId: String?, sinceId: String?
    ): List<ChatStatus> {
        var placeholderToInsert: Placeholder? = null

        // Look for overlap
        val resultChats = if (chats.isNotEmpty() && sinceId != null) {
        val resultChats = if(chats.isNotEmpty() && sinceId != null) {
            val indexOfSince = chats.indexOfLast { it.id == sinceId }
            if (indexOfSince == -1) {
            if(indexOfSince == -1) {
                // We didn't find the status which must be there. Add a placeholder
                placeholderToInsert = Placeholder(sinceId.inc())
                chats.mapTo(mutableListOf(), Chat::lift)
                        .apply {
                            add(Either.Left(placeholderToInsert))
                        }
                    .apply {
                        add(Either.Left(placeholderToInsert))
                    }
            } else {
                // There was an overlap. Remove all overlapped statuses. No need for a placeholder.
                chats.mapTo(mutableListOf(), Chat::lift)
                        .apply {
                            subList(indexOfSince, size).clear()
                        }
                    .apply {
                        subList(indexOfSince, size).clear()
                    }
            }
        } else {
            // Just a normal case.


@@ 160,13 211,13 @@ class ChatRepositoryImpl(
                chatsDao.deleteRange(accountId, chats.last().id, chats.first().id)
            }

            for (chat in chats) {
            for(chat in chats) {
                val pair = chat.toEntity(accountId, gson)

                chatsDao.insertInTransaction(
                        pair.first,
                        pair.second,
                        chat.account.toEntity(accountId, gson)
                    pair.first,
                    pair.second,
                    chat.account.toEntity(accountId, gson)
                )
            }



@@ 176,21 227,24 @@ class ChatRepositoryImpl(

            // If we're loading in the bottom insert placeholder after every load
            // (for requests on next launches) but not return it.
            if (sinceId == null && chats.isNotEmpty()) {
            if(sinceId == null && chats.isNotEmpty()) {
                chatsDao.insertChatIfNotThere(
                        Placeholder(chats.last().id.dec()).toChatEntity(accountId))
                    Placeholder(chats.last().id.dec()).toChatEntity(accountId)
                )
            }

            // There may be placeholders which we thought could be from our TL but they are not
            if (chats.size > 2) {
                chatsDao.removeAllPlaceholdersBetween(accountId, chats.first().id,
                        chats.last().id)
            } else if (placeholderToInsert == null && maxId != null && sinceId != null) {
            if(chats.size > 2) {
                chatsDao.removeAllPlaceholdersBetween(
                    accountId, chats.first().id,
                    chats.last().id
                )
            } else if(placeholderToInsert == null && maxId != null && sinceId != null) {
                chatsDao.removeAllPlaceholdersBetween(accountId, maxId, sinceId)
            }
        }
                .subscribeOn(Schedulers.io())
                .subscribe()
            .subscribeOn(Schedulers.io())
            .subscribe()

        return resultChats
    }


@@ 200,62 254,73 @@ private val emojisListTypeToken = object : TypeToken<List<Emoji>>() {}

fun Placeholder.toChatEntity(timelineUserId: Long): ChatEntity {
    return ChatEntity(
            localId = timelineUserId,
            chatId = this.id,
            accountId = "",
            unread = 0L,
            updatedAt = 0L,
            lastMessageId = null
        localId = timelineUserId,
        chatId = this.id,
        accountId = "",
        unread = 0L,
        updatedAt = 0L,
        lastMessageId = null
    )
}

fun ChatMessage.toEntity(timelineUserId: Long, gson: Gson) : ChatMessageEntity {
fun ChatMessage.toEntity(timelineUserId: Long, gson: Gson): ChatMessageEntity {
    return ChatMessageEntity(
            localId = timelineUserId,
            messageId = this.id,
            content = this.content?.toHtml(),
            chatId = this.chatId,
            accountId = this.accountId,
            createdAt = this.createdAt.time,
            attachment = this.attachment?.let { gson.toJson(it, Attachment::class.java) },
            emojis = gson.toJson(this.emojis)
        localId = timelineUserId,
        messageId = this.id,
        content = this.content?.toHtml(),
        chatId = this.chatId,
        accountId = this.accountId,
        createdAt = this.createdAt.time,
        attachment = this.attachment?.let { gson.toJson(it, Attachment::class.java) },
        emojis = gson.toJson(this.emojis)
    )
}

fun Chat.toEntity(timelineUserId: Long, gson: Gson): Pair<ChatEntity, ChatMessageEntity?> {
    return Pair(ChatEntity(
    return Pair(
        ChatEntity(
            localId = timelineUserId,
            chatId = this.id,
            accountId = this.account.id,
            unread = this.unread,
            updatedAt = this.updatedAt.time,
            lastMessageId = this.lastMessage?.id
    ), this.lastMessage?.toEntity(timelineUserId, gson))
        ), this.lastMessage?.toEntity(timelineUserId, gson)
    )
}

fun ChatMessageEntity.toChatMessage(gson: Gson) : ChatMessage {
fun ChatMessageEntity.toChatMessage(gson: Gson): ChatMessage {
    return ChatMessage(
            id = this.messageId,
            content = this.content?.let { it.parseAsHtml().trimTrailingWhitespace() },
            chatId = this.chatId,
            accountId = this.accountId,
            createdAt = Date(this.createdAt),
            attachment = this.attachment?.let { gson.fromJson(it, Attachment::class.java) },
            emojis = gson.fromJson(this.emojis, object : TypeToken<List<Emoji>>() {}.type ),
            card = null /* don't care about card */
        id = this.messageId,
        content = this.content?.let { it.parseAsHtml().trimTrailingWhitespace() },
        chatId = this.chatId,
        accountId = this.accountId,
        createdAt = Date(this.createdAt),
        attachment = this.attachment?.let { gson.fromJson(it, Attachment::class.java) },
        emojis = gson.fromJson(this.emojis, object : TypeToken<List<Emoji>>() {}.type),
        card = null /* don't care about card */
    )
}

fun ChatEntityWithAccount.toChat(gson: Gson) : ChatStatus {
fun ChatEntityWithAccount.toChat(gson: Gson): ChatStatus {
    if(account == null || chat.accountId.isEmpty() || chat.updatedAt == 0L)
        return Either.Left(Placeholder(chat.chatId))

    return Chat(
            account = this.account?.toAccount(gson) ?: Account("", "", "", "", SpannedString(""), "", "", "" ),
            id = this.chat.chatId,
            unread = this.chat.unread,
            updatedAt = Date(this.chat.updatedAt),
            lastMessage = this.lastMessage?.toChatMessage(gson)
        account = this.account?.toAccount(gson) ?: Account(
            "",
            "",
            "",
            "",
            SpannedString(""),
            "",
            "",
            ""
        ),
        id = this.chat.chatId,
        unread = this.chat.unread,
        updatedAt = Date(this.chat.updatedAt),
        lastMessage = this.lastMessage?.toChatMessage(gson)
    ).lift()
}