diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/husky/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index b1a794a..a85b60a 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/husky/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -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 . */ + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ 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> @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> @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> @GET("api/v1/timelines/tag/{hashtag}?with_muted=true") fun hashtagTimeline( - @Path("hashtag") hashtag: String, - @Query("any[]") any: List?, - @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?, + @Query("local") local: Boolean?, + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? ): Call> @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> @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?, - @Query("with_muted") withMuted: Boolean? + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int?, + @Query("exclude_types[]") excludes: Set?, + @Query("with_muted") withMuted: Boolean? ): Call> @GET("api/v1/markers") fun markersWithAuth( - @Header("Authorization") auth: String, - @Header(DOMAIN_HEADER) domain: String, - @Query("timeline[]") timelines: List + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Query("timeline[]") timelines: List ): Single> @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? + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Query("since_id") sinceId: String?, + @Query("include_types[]") includeTypes: List? ): Single> @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 @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 @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 @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 @GET("api/v1/statuses/{id}") fun status( - @Path("id") statusId: String + @Path("id") statusId: String ): Call @GET("api/v1/statuses/{id}") fun statusSingle( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @GET("api/v1/statuses/{id}/context") fun statusContext( - @Path("id") statusId: String + @Path("id") statusId: String ): Call @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>> @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>> @DELETE("api/v1/statuses/{id}") fun deleteStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/reblog") fun reblogStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/unreblog") fun unreblogStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/favourite") fun favouriteStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/unfavourite") fun unfavouriteStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/bookmark") fun bookmarkStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/unbookmark") fun unbookmarkStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/pin") fun pinStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/unpin") fun unpinStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single - + @POST("api/v1/statuses/{id}/mute") fun muteConversation( - @Path("id") statusId: String + @Path("id") statusId: String ): Single - + @POST("api/v1/statuses/{id}/unmute") fun unmuteConversation( - @Path("id") statusId: String + @Path("id") statusId: String ): Single - + @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> @DELETE("api/v1/scheduled_statuses/{id}") fun deleteScheduledStatus( - @Path("id") scheduledStatusId: String + @Path("id") scheduledStatusId: String ): Single @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 @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 @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> @GET("api/v1/accounts/{id}") fun account( - @Path("id") accountId: String + @Path("id") accountId: String ): Single /** @@ -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> @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>> @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>> @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 @POST("api/v1/accounts/{id}/unfollow") fun unfollowAccount( - @Path("id") accountId: String + @Path("id") accountId: String ): Single @POST("api/v1/accounts/{id}/block") fun blockAccount( - @Path("id") accountId: String + @Path("id") accountId: String ): Single @POST("api/v1/accounts/{id}/unblock") fun unblockAccount( - @Path("id") accountId: String + @Path("id") accountId: String ): Single @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 @POST("api/v1/accounts/{id}/unmute") fun unmuteAccount( - @Path("id") accountId: String + @Path("id") accountId: String ): Single @GET("api/v1/accounts/relationships") fun relationships( - @Query("id[]") accountIds: List + @Query("id[]") accountIds: List ): Single> @GET("api/v1/accounts/{id}/identity_proofs") fun identityProofs( - @Path("id") accountId: String + @Path("id") accountId: String ): Single> - + @POST("api/v1/pleroma/accounts/{id}/subscribe") fun subscribeAccount( @Path("id") accountId: String ): Single - + @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>> @GET("api/v1/mutes") fun mutes( - @Query("max_id") maxId: String? + @Query("max_id") maxId: String? ): Single>> @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>> @FormUrlEncoded @POST("api/v1/domain_blocks") fun blockDomain( - @Field("domain") domain: String + @Field("domain") domain: String ): Call @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> @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> @GET("api/v1/follow_requests") fun followRequests( - @Query("max_id") maxId: String? + @Query("max_id") maxId: String? ): Single>> @POST("api/v1/follow_requests/{id}/authorize") fun authorizeFollowRequest( - @Path("id") accountId: String + @Path("id") accountId: String ): Call @POST("api/v1/follow_requests/{id}/reject") fun rejectFollowRequest( - @Path("id") accountId: String + @Path("id") accountId: String ): Call @POST("api/v1/follow_requests/{id}/authorize") fun authorizeFollowRequestObservable( - @Path("id") accountId: String + @Path("id") accountId: String ): Single @POST("api/v1/follow_requests/{id}/reject") fun rejectFollowRequestObservable( - @Path("id") accountId: String + @Path("id") accountId: String ): Single @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 @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 @FormUrlEncoded @POST("api/v1/lists") fun createList( - @Field("title") title: String + @Field("title") title: String ): Single @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 @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> @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 + @Path("listId") listId: String, + @Field("account_ids[]") accountIds: List ): Completable @FormUrlEncoded @POST("api/v1/lists/{listId}/accounts") fun addCountToList( - @Path("listId") listId: String, - @Field("account_ids[]") accountIds: List + @Path("listId") listId: String, + @Field("account_ids[]") accountIds: List ): 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> 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 @DELETE("api/v1/filters/{id}") fun deleteFilter( - @Path("id") id: String + @Path("id") id: String ): Call @FormUrlEncoded @POST("api/v1/polls/{id}/votes") fun voteInPoll( - @Path("id") id: String, - @Field("choices[]") choices: List + @Path("id") id: String, + @Field("choices[]") choices: List ): Single @GET("api/v1/announcements") fun listAnnouncements( - @Query("with_dismissed") withDismissed: Boolean = true + @Query("with_dismissed") withDismissed: Boolean = true ): Single> @POST("api/v1/announcements/{id}/dismiss") fun dismissAnnouncement( - @Path("id") announcementId: String + @Path("id") announcementId: String ): Single @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 @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 @FormUrlEncoded @POST("api/v1/reports") fun reportObservable( - @Field("account_id") accountId: String, - @Field("status_ids[]") statusIds: List, - @Field("comment") comment: String, - @Field("forward") isNotifyRemote: Boolean? + @Field("account_id") accountId: String, + @Field("status_ids[]") statusIds: List, + @Field("comment") comment: String, + @Field("forward") isNotifyRemote: Boolean? ): Single @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> @GET("api/v1/statuses/{id}") fun statusObservable( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @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 - + @GET(".well-known/nodeinfo") - fun getNodeinfoLinks() : Single - + fun getNodeinfoLinks(): Single + @GET - fun getNodeinfo(@Url url: String) : Single - + fun getNodeinfo(@Url url: String): Single + @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> + fun getStickers(): Single> @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 @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 @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> @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> @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 @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 @POST("api/v1/pleroma/chats/by-account-id/{id}") fun createChat( - @Path("id") accountId: String + @Path("id") accountId: String ): Single @GET("api/v1/pleroma/chats/{id}") fun getChat( - @Path("id") chatId: String + @Path("id") chatId: String ): Single @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 } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/repository/ChatRepository.kt b/husky/app/src/main/java/com/keylesspalace/tusky/repository/ChatRepository.kt index 6fa19c3..4c03a2c 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/repository/ChatRepository.kt +++ b/husky/app/src/main/java/com/keylesspalace/tusky/repository/ChatRepository.kt @@ -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 . + */ + 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 typealias ChatMesssageOrPlaceholder = Either interface ChatRepository { - fun getChats(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, - requestMode: TimelineRequestMode): Single> + fun getChats( + maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, + requestMode: TimelineRequestMode + ): Single> - fun getChatMessages(chatId: String, maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, requestMode: TimelineRequestMode) : Single> + fun getChatMessages( + chatId: String, + maxId: String?, + sinceId: String?, + sincedIdMinusOne: String?, + limit: Int, + requestMode: TimelineRequestMode + ): Single> } 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> { 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> { + override fun getChatMessages( + chatId: String, + maxId: String?, + sinceId: String?, + sincedIdMinusOne: String?, + limit: Int, + requestMode: TimelineRequestMode + ): Single> { 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> { 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> { 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, - maxId: String?, sinceId: String?, limit: Int, - requestMode: TimelineRequestMode + private fun addFromDbIfNeeded( + accountId: Long, chats: List, + maxId: String?, sinceId: String?, limit: Int, + requestMode: TimelineRequestMode ): Single> { - 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> { + private fun getChatsFromDb( + accountId: Long, maxId: String?, sinceId: String?, + limit: Int + ): Single> { 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, - maxId: String?, sinceId: String? + private fun saveChatsToDb( + accountId: Long, chats: List, + maxId: String?, sinceId: String? ): List { 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>() {} 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 { - 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>() {}.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>() {}.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() }