Skip to content

Commit

Permalink
Merge pull request #413 from inaturalist/3896-announcement-platforms
Browse files Browse the repository at this point in the history
Announcement clients
  • Loading branch information
pleary authored Oct 25, 2023
2 parents e9a8a42 + aeb0e7f commit 08a61b9
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 6 deletions.
14 changes: 13 additions & 1 deletion lib/controllers/v1/announcements_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ const { announcements } = require( "inaturalistjs" );
const InaturalistAPI = require( "../../inaturalist_api" );
const pgClient = require( "../../pg_client" );
const Site = require( "../../models/site" );
const util = require( "../../util" );

const AnnouncementsController = class AnnouncementsController {
static async search( req ) {
let query = squel.select( )
.field( "announcements.id, body, placement, dismissible, locales, start, \"end\"" )
.field( "announcements.id, body, placement, dismissible, locales, clients, start, \"end\"" )
.from( "announcements" )
.where( "NOW() at time zone 'utc' between start and \"end\"" )
.order( "announcements.id" );
Expand All @@ -29,6 +30,17 @@ const AnnouncementsController = class AnnouncementsController {
query = query.where( placementClause );
}

const userAgentClient = util.userAgentClient( req );
if ( req.query.client || userAgentClient ) {
// given a client parameter, return only announcements that include that client,
// or announcements with no client specified
query = query.where( "? = ANY( clients ) OR clients IS NULL OR clients = '{}'",
req.query.client || userAgentClient );
} else {
// if there is no client parameter, return only announcements with no client specified
query = query.where( "clients IS NULL OR clients = '{}'" );
}

// site_id filter
if ( req.userSession ) {
// authenticated requests include announcements targeted at the users site,
Expand Down
21 changes: 21 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,27 @@ const util = class util {
}
return arr[Math.floor( length / 2 )];
}

static userAgentClient( req ) {
const userAgent = req.headers["user-agent"];
if ( _.isEmpty( userAgent ) ) {
return null;
}
if ( userAgent.match( /iNaturalistReactNative\// )
|| userAgent.match( /iNaturalistRN\// ) ) {
return "inatrn";
}
if ( userAgent.match( /Seek\// ) ) {
return "seek";
}
if ( userAgent.match( /iNaturalist\/.*Darwin/ ) ) {
return "inat-ios";
}
if ( userAgent.match( /iNaturalist\/.*Android/ ) ) {
return "inat-android";
}
return null;
}
};

util.iucnValues = {
Expand Down
6 changes: 6 additions & 0 deletions openapi/schema/request/announcements_search.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ module.exports = Joi.object( ).keys( {
"mobile/home",
"mobile"
),
client: Joi.string( ).valid(
"inat-ios",
"inat-android",
"seek",
"inatrn"
),
locale: Joi.string( ),
fields: Joi.any( )
} ).unknown( false );
1 change: 1 addition & 0 deletions openapi/schema/response/announcement.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module.exports = Joi.object( ).keys( {
id: Joi.number( ).integer( ).required( ),
body: Joi.string( ),
placement: Joi.string( ),
clients: Joi.array( ).items( Joi.string( ) ),
dismissible: Joi.boolean( ),
locales: Joi.array( ).items( Joi.string( ) ),
start: Joi.date( ),
Expand Down
3 changes: 2 additions & 1 deletion schema/database.sql
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,8 @@ CREATE TABLE public.announcements (
updated_at timestamp without time zone,
locales text[] DEFAULT '{}'::text[],
dismiss_user_ids integer[] DEFAULT '{}'::integer[],
dismissible boolean DEFAULT false
dismissible boolean DEFAULT false,
clients text[] DEFAULT '{}'::text[]
);


Expand Down
24 changes: 20 additions & 4 deletions schema/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -1953,7 +1953,8 @@
"updated_at": "2023-01-01 00:00:00",
"placement": "mobile/home",
"locales": "{}",
"dismissible": true
"dismissible": true,
"clients": "{}"
},
{
"id": 2,
Expand All @@ -1964,7 +1965,8 @@
"updated_at": "2023-01-01 00:00:00",
"placement": "mobile/home",
"locales": "{}",
"dismissible": true
"dismissible": true,
"clients": "{}"
},
{
"id": 3,
Expand All @@ -1975,7 +1977,8 @@
"updated_at": "2023-01-01 00:00:00",
"placement": "mobile/home",
"locales": "{en-US,fr}",
"dismissible": true
"dismissible": true,
"clients": "{}"
},
{
"id": 4,
Expand All @@ -1986,7 +1989,20 @@
"updated_at": "2023-01-01 00:00:00",
"placement": "mobile/home",
"locales": "{en}",
"dismissible": true
"dismissible": true,
"clients": "{}"
},
{
"id": 5,
"body": "Active announcement for inat-ios and inat-android",
"start": "2023-01-01 00:00:00",
"end": "2100-01-01 00:00:00",
"created_at": "2023-01-01 00:00:00",
"updated_at": "2023-01-01 00:00:00",
"placement": "mobile/home",
"locales": "{}",
"dismissible": true,
"clients": "{inat-ios,inat-android}"
}
],
"comments": [
Expand Down
52 changes: 52 additions & 0 deletions test/integration/v2/announcements.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,58 @@ describe( "Announcements", ( ) => {
} ).expect( "Content-Type", /json/ )
.expect( 200, done );
} );

it( "returns announcements based on client", function ( done ) {
const inatiOSAnnouncement = _.find(
fixtures.postgresql.announcements, a => a.clients.match( /inat-ios/ )
);
request( this.app ).get( "/v2/announcements?fields=all&client=inat-ios" ).expect( res => {
expect( res.body.results ).to.not.be.empty;
expect( _.every( res.body.results, r => (
r.clients.includes( "inat-ios" ) || _.isEmpty( r.clients )
) ) ).to.be.true;
expect( _.map( res.body.results, "id" ) ).to.include( inatiOSAnnouncement.id );
} ).expect( "Content-Type", /json/ )
.expect( 200, done );
} );

it( "returns announcements based on user agent", function ( done ) {
const inatiOSAnnouncement = _.find(
fixtures.postgresql.announcements, a => a.clients.match( /inat-ios/ )
);
request( this.app )
.get( "/v2/announcements?fields=all" )
.set( "User-Agent", "iNaturalist/708 CFNetwork/1410.0.3 Darwin/22.6.0" )
.expect( res => {
expect( res.body.results ).to.not.be.empty;
expect( _.every( res.body.results, r => (
r.clients.includes( "inat-ios" ) || _.isEmpty( r.clients )
) ) ).to.be.true;
expect( _.map( res.body.results, "id" ) ).to.include( inatiOSAnnouncement.id );
} )
.expect( "Content-Type", /json/ )
.expect( 200, done );
} );

it( "does not return announcements with a client not matching parameter", function ( done ) {
request( this.app ).get( "/v2/announcements?fields=all&client=seek" ).expect( res => {
expect( res.body.results ).to.not.be.empty;
expect( _.every( res.body.results, r => _.isEmpty( r.clients ) ) ).to.be.true;
} ).expect( "Content-Type", /json/ )
.expect( 200, done );
} );

it( "does not return announcements with a client not matching parameter", function ( done ) {
request( this.app )
.get( "/v2/announcements?fields=all" )
.set( "User-Agent", "Seek/2.15.3 Handset (Build 316) Android/13" )
.expect( res => {
expect( res.body.results ).to.not.be.empty;
expect( _.every( res.body.results, r => _.isEmpty( r.clients ) ) ).to.be.true;
} )
.expect( "Content-Type", /json/ )
.expect( 200, done );
} );
} );

describe( "dismiss", ( ) => {
Expand Down
54 changes: 54 additions & 0 deletions test/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,58 @@ describe( "util", ( ) => {
expect( opts.locale ).to.eq( "he-il" ); // not sure why it's lowercase...
} );
} );

describe( "userAgentClient", ( ) => {
it( "returns nil when request user agent is empty", ( ) => {
expect( util.userAgentClient( { headers: { } } ) ).to.be.null;
expect( util.userAgentClient( { headers: { "user-agent": null } } ) ).to.be.null;
expect( util.userAgentClient( { headers: { "user-agent": "" } } ) ).to.be.null;
} );

it( "returns nil when request user agent is unrecognized", ( ) => {
expect( util.userAgentClient( { headers: { "user-agent": "nonsense" } } ) ).to.be.null;
} );

it( "recognizes inatrn client user agents", ( ) => {
expect( util.userAgentClient( {
headers: {
"user-agent": "iNaturalistReactNative/60 CFNetwork/1474 Darwin/23.0.0"
}
} ) ).to.eq( "inatrn" );
expect( util.userAgentClient( {
headers: {
"user-agent": "iNaturalistRN/0.14.0 Handset (Build 60) iOS/17.0.3"
}
} ) ).to.eq( "inatrn" );
} );

it( "recognizes seek client user agents", ( ) => {
expect( util.userAgentClient( {
headers: {
"user-agent": "Seek/2.15.3 Handset (Build 316) iOS/17.0.3"
}
} ) ).to.eq( "seek" );
expect( util.userAgentClient( {
headers: {
"user-agent": "Seek/2.15.3 Handset (Build 316) Android/13"
}
} ) ).to.eq( "seek" );
} );

it( "recognizes inat-ios client user agents", ( ) => {
expect( util.userAgentClient( {
headers: {
"user-agent": "iNaturalist/708 CFNetwork/1410.0.3 Darwin/22.6.0"
}
} ) ).to.eq( "inat-ios" );
} );

it( "recognizes inat-android client user agents", ( ) => {
expect( util.userAgentClient( {
headers: {
"user-agent": "iNaturalist/1.29.18 (Build 592; Android 5.10.157-android13-4-00001-g5c7ff5dc7aac-ab10381520 10754064; SDK 34; bluejay Pixel 6a bluejay; OS Version 14)"
}
} ) ).to.eq( "inat-android" );
} );
} );
} );

0 comments on commit 08a61b9

Please sign in to comment.