Formatting, license header

This commit is contained in:
Adolfo Santiago 2022-01-15 20:36:53 +01:00
parent 237241baa1
commit 6d8aa8ff47
No known key found for this signature in database
GPG key ID: 244D6F9A317B4A65
2 changed files with 429 additions and 319 deletions

View file

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

View file

@ -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 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 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))
}
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 */
), this.lastMessage?.toEntity(timelineUserId, gson)
)
}
fun ChatEntityWithAccount.toChat(gson: Gson) : ChatStatus {
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 */
)
}
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()
}