Move notification API into main commons codebase (#5465)

* Moved the notification API calls out of the data client

* Converted the NofificationClient to kotlin and improved its test
This commit is contained in:
Paul Hawke 2024-01-23 07:43:37 -06:00 committed by GitHub
parent 1948bab873
commit 3c1cdf18a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 210 additions and 94 deletions

View file

@ -19,6 +19,7 @@ import fr.free.nrw.commons.media.PageMediaInterface;
import fr.free.nrw.commons.media.WikidataMediaInterface;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.mwapi.UserInterface;
import fr.free.nrw.commons.notification.NotificationInterface;
import fr.free.nrw.commons.review.ReviewInterface;
import fr.free.nrw.commons.upload.UploadInterface;
import fr.free.nrw.commons.upload.WikiBaseInterface;
@ -265,6 +266,14 @@ public class NetworkingModule {
.get(commonsWikiSite, BuildConfig.COMMONS_URL, ThanksInterface.class);
}
@Provides
@Singleton
public NotificationInterface provideNotificationInterface(
@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
return ServiceFactory
.get(commonsWikiSite, BuildConfig.COMMONS_URL, NotificationInterface.class);
}
@Provides
@Singleton
public UserInterface provideUserInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {

View file

@ -1,46 +0,0 @@
package fr.free.nrw.commons.notification;
import fr.free.nrw.commons.notification.models.Notification;
import org.wikipedia.csrf.CsrfTokenClient;
import org.wikipedia.dataclient.Service;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import io.reactivex.Observable;
import io.reactivex.Single;
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
@Singleton
public class NotificationClient {
private final Service service;
private final CsrfTokenClient csrfTokenClient;
@Inject
public NotificationClient(@Named("commons-service") Service service, @Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) {
this.service = service;
this.csrfTokenClient = csrfTokenClient;
}
public Single<List<Notification>> getNotifications(boolean archived) {
return service.getAllNotifications("wikidatawiki|commonswiki|enwiki", archived ? "read" : "!read", null)
.map(mwQueryResponse -> mwQueryResponse.query().notifications().list())
.flatMap(Observable::fromIterable)
.map(notification -> Notification.from(notification))
.toList();
}
public Observable<Boolean> markNotificationAsRead(String notificationId) {
try {
return service.markRead(csrfTokenClient.getTokenBlocking(), notificationId, "")
.map(mwQueryResponse -> mwQueryResponse.success());
} catch (Throwable throwable) {
return Observable.just(false);
}
}
}

View file

@ -0,0 +1,54 @@
package fr.free.nrw.commons.notification
import fr.free.nrw.commons.di.NetworkingModule
import fr.free.nrw.commons.notification.models.Notification
import fr.free.nrw.commons.notification.models.NotificationType
import io.reactivex.Observable
import io.reactivex.Single
import org.wikipedia.csrf.CsrfTokenClient
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.util.DateUtil
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
import org.wikipedia.notifications.Notification as WikimediaNotification
@Singleton
class NotificationClient @Inject constructor(
@param:Named(NetworkingModule.NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient,
private val service: NotificationInterface
) {
fun getNotifications(archived: Boolean): Single<List<Notification>> =
service.getAllNotifications(
wikiList = "wikidatawiki|commonswiki|enwiki",
filter = if (archived) "read" else "!read",
continueStr = null
).map {
it.query()?.notifications()?.list() ?: emptyList()
}.flatMap {
Observable.fromIterable(it)
}.map {
it.toCommonsNotification()
}.toList()
fun markNotificationAsRead(notificationId: String?): Observable<Boolean> {
return try {
service.markRead(
token = csrfTokenClient.tokenBlocking,
readList = notificationId,
unreadList = ""
).map(MwQueryResponse::success)
} catch (throwable: Throwable) {
Observable.just(false)
}
}
private fun WikimediaNotification.toCommonsNotification() = Notification(
notificationType = NotificationType.UNKNOWN,
notificationText = contents?.compactHeader ?: "",
date = DateUtil.getMonthOnlyDateString(timestamp),
link = contents?.links?.primary?.url ?: "",
iconUrl = "",
notificationId = id().toString()
)
}

View file

@ -0,0 +1,31 @@
package fr.free.nrw.commons.notification
import io.reactivex.Observable
import org.wikipedia.dataclient.Service
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.Query
interface NotificationInterface {
@Headers("Cache-Control: no-cache")
@GET(Service.MW_API_PREFIX + "action=query&meta=notifications&notformat=model&notlimit=max")
fun getAllNotifications(
@Query("notwikis") wikiList: String?,
@Query("notfilter") filter: String?,
@Query("notcontinue") continueStr: String?
): Observable<MwQueryResponse?>
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST(Service.MW_API_PREFIX + "action=echomarkread")
fun markRead(
@Field("token") token: String,
@Field("list") readList: String?,
@Field("unreadlist") unreadList: String?
): Observable<MwQueryResponse?>
}

View file

@ -1,29 +1,13 @@
package fr.free.nrw.commons.notification.models
import org.wikipedia.util.DateUtil
/**
* Created by root on 18.12.2017.
*/
data class Notification(var notificationType: NotificationType,
var notificationText: String,
var date: String,
var link: String,
var iconUrl: String,
var notificationId: String) {
companion object {
@JvmStatic
fun from(wikiNotification: org.wikipedia.notifications.Notification): Notification {
val contents = wikiNotification.contents
val notificationLink = if (contents == null || contents.links == null || contents.links!!.primary == null) "" else contents.links!!.primary!!.url
return Notification(
NotificationType.UNKNOWN,
contents?.compactHeader ?: "",
DateUtil.getMonthOnlyDateString(wikiNotification.timestamp),
notificationLink,
"", wikiNotification.id().toString())
}
}
}
data class Notification(
var notificationType: NotificationType,
var notificationText: String,
var date: String,
var link: String,
var iconUrl: String,
var notificationId: String
)

View file

@ -1,35 +1,47 @@
package fr.free.nrw.commons.notification
import androidx.test.ext.junit.runners.AndroidJUnit4
import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.notification.models.NotificationType
import io.reactivex.Observable
import junit.framework.TestCase.assertEquals
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import org.wikipedia.csrf.CsrfTokenClient
import org.wikipedia.dataclient.Service
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.dataclient.mwapi.MwQueryResult
import org.wikipedia.json.GsonUtil
import org.wikipedia.notifications.Notification
@RunWith(AndroidJUnit4::class)
@Config(sdk = [21], application = TestCommonsApplication::class)
@LooperMode(LooperMode.Mode.PAUSED)
class NotificationClientTest {
@Mock
private lateinit var service: Service
private lateinit var service: NotificationInterface
@Mock
private lateinit var csrfTokenClient: CsrfTokenClient
@Mock
private lateinit var mQueryResponse: MwQueryResponse
@Mock
private lateinit var mQueryResult: MwQueryResult
@Mock
private lateinit var mQueryResultNotificationsList: MwQueryResult.NotificationList
@Mock
private lateinit var notificationsList: List<Notification>
private lateinit var notificationClient: NotificationClient
@ -39,8 +51,8 @@ class NotificationClientTest {
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
notificationClient = NotificationClient(service, csrfTokenClient)
MockitoAnnotations.openMocks(this)
notificationClient = NotificationClient(csrfTokenClient, service)
}
/**
@ -49,12 +61,40 @@ class NotificationClientTest {
@Test
fun getNotificationTest() {
Mockito.`when`(service.getAllNotifications(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.any())).thenReturn(Observable.just(mQueryResponse))
Mockito.`when`(service.getAllNotifications(anyString(), anyString(), any()))
.thenReturn(Observable.just(mQueryResponse))
Mockito.`when`(mQueryResponse.query()).thenReturn(mQueryResult)
Mockito.`when`(mQueryResult.notifications()).thenReturn(mQueryResultNotificationsList)
Mockito.`when`(mQueryResultNotificationsList.list()).thenReturn(notificationsList)
notificationClient.getNotifications(true)
verify(service).getAllNotifications(eq("wikidatawiki|commonswiki|enwiki"), eq("read"), eq(null))
Mockito.`when`(mQueryResultNotificationsList.list()).thenReturn(
listOf(
createWikimediaNotification(
primaryUrl = "foo",
compactHeader = "header",
timestamp = "2024-01-22T10:12:00Z",
notificationId = 1234L
)
)
)
val result = notificationClient.getNotifications(true).test().values()
verify(service).getAllNotifications(
eq("wikidatawiki|commonswiki|enwiki"),
eq("read"),
eq(null)
)
val notificationList = result.first()
assertEquals(1, notificationList.size)
with(notificationList.first()) {
assertEquals(NotificationType.UNKNOWN, notificationType)
assertEquals("header", notificationText)
assertEquals("January 22", date)
assertEquals("foo", link)
assertEquals("", iconUrl)
assertEquals("1234", notificationId)
}
}
/**
@ -63,10 +103,33 @@ class NotificationClientTest {
@Test
fun markNotificationAsReadTest() {
Mockito.`when`(csrfTokenClient.tokenBlocking).thenReturn("test")
Mockito.`when`(service.markRead(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())).thenReturn(Observable.just(mQueryResponse))
Mockito.`when`(service.markRead(anyString(), anyString(), anyString()))
.thenReturn(Observable.just(mQueryResponse))
Mockito.`when`(mQueryResponse.success()).thenReturn(true)
notificationClient.markNotificationAsRead("test")
verify(service).markRead(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())
verify(service).markRead(anyString(), anyString(), anyString())
}
@Suppress("SameParameterValue")
private fun createWikimediaNotification(
primaryUrl: String, compactHeader: String, timestamp: String, notificationId: Long
) = Notification().apply {
setId(notificationId)
setTimestamp(Notification.Timestamp().apply {
setUtciso8601(timestamp)
})
contents = Notification.Contents().apply {
setCompactHeader(compactHeader)
links = Notification.Links().apply {
setPrimary(
GsonUtil.getDefaultGson().toJsonTree(Notification.Link().apply {
setUrl(primaryUrl)
})
)
}
}
}
}

View file

@ -244,17 +244,6 @@ public interface Service {
// ------- Notifications -------
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=notifications&notformat=model&notlimit=max")
@NonNull Observable<MwQueryResponse> getAllNotifications(@Query("notwikis") @Nullable String wikiList,
@Query("notfilter") @Nullable String filter,
@Query("notcontinue") @Nullable String continueStr);
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=echomarkread")
@NonNull Observable<MwQueryResponse> markRead(@Field("token") @NonNull String token, @Field("list") @Nullable String readList, @Field("unreadlist") @Nullable String unreadList);
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=notifications&notprop=list&notfilter=!read&notlimit=1")
@NonNull Observable<MwQueryResponse> getLastUnreadNotification();

View file

@ -48,6 +48,10 @@ public class Notification {
return id;
}
public void setId(final long id) {
this.id = id;
}
public long key() {
return id + wiki().hashCode();
}
@ -72,10 +76,18 @@ public class Notification {
return contents;
}
public void setContents(@Nullable final Contents contents) {
this.contents = contents;
}
@NonNull public Date getTimestamp() {
return timestamp != null ? timestamp.date() : new Date();
}
public void setTimestamp(@Nullable final Timestamp timestamp) {
this.timestamp = timestamp;
}
@NonNull String getUtcIso8601() {
return StringUtils.defaultString(timestamp != null ? timestamp.utciso8601 : null);
}
@ -127,6 +139,10 @@ public class Notification {
public static class Timestamp {
@SuppressWarnings("unused,NullableProblems") @Nullable private String utciso8601;
public void setUtciso8601(@Nullable final String utciso8601) {
this.utciso8601 = utciso8601;
}
public Date date() {
try {
return DateUtil.iso8601DateParse(utciso8601);
@ -148,6 +164,10 @@ public class Notification {
return StringUtils.defaultString(url);
}
public void setUrl(@Nullable final String url) {
this.url = url;
}
@NonNull public String getTooltip() {
return StringUtils.defaultString(tooltip);
}
@ -166,6 +186,10 @@ public class Notification {
@SuppressWarnings("unused,NullableProblems") @Nullable private List<Link> secondary;
private Link primaryLink;
public void setPrimary(@Nullable final JsonElement primary) {
this.primary = primary;
}
@Nullable public Link getPrimary() {
if (primary == null) {
return null;
@ -215,6 +239,10 @@ public class Notification {
return StringUtils.defaultString(compactHeader);
}
public void setCompactHeader(@Nullable final String compactHeader) {
this.compactHeader = compactHeader;
}
@NonNull public String getBody() {
return StringUtils.defaultString(body);
}
@ -226,6 +254,10 @@ public class Notification {
@Nullable public Links getLinks() {
return links;
}
public void setLinks(@Nullable final Links links) {
this.links = links;
}
}
public static class UnreadNotificationWikiItem {