Skip to content

Commit

Permalink
rename announcement platforms to clients; add auto-detection of annou…
Browse files Browse the repository at this point in the history
…ncement client using user agent
  • Loading branch information
pleary committed Oct 20, 2023
1 parent 4949956 commit aeb0e7f
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 22 deletions.
17 changes: 10 additions & 7 deletions 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, platforms, 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,13 +30,15 @@ const AnnouncementsController = class AnnouncementsController {
query = query.where( placementClause );
}

if ( req.query.platform ) {
// given a platform parameter, return only announcements that include that platform,
// or announcements with no platform specified
query = query.where( "? = ANY( platforms ) OR platforms IS NULL OR platforms = '{}'", req.query.platform );
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 platform parameter, return only announcements with no platform specified
query = query.where( "platforms IS NULL OR platforms = '{}'" );
// if there is no client parameter, return only announcements with no client specified
query = query.where( "clients IS NULL OR clients = '{}'" );
}

// site_id filter
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
2 changes: 1 addition & 1 deletion openapi/schema/request/announcements_search.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = Joi.object( ).keys( {
"mobile/home",
"mobile"
),
platform: Joi.string( ).valid(
client: Joi.string( ).valid(
"inat-ios",
"inat-android",
"seek",
Expand Down
2 changes: 1 addition & 1 deletion openapi/schema/response/announcement.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = Joi.object( ).keys( {
id: Joi.number( ).integer( ).required( ),
body: Joi.string( ),
placement: Joi.string( ),
platforms: Joi.array( ).items( Joi.string( ) ),
clients: Joi.array( ).items( Joi.string( ) ),
dismissible: Joi.boolean( ),
locales: Joi.array( ).items( Joi.string( ) ),
start: Joi.date( ),
Expand Down
2 changes: 1 addition & 1 deletion schema/database.sql
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ CREATE TABLE public.announcements (
locales text[] DEFAULT '{}'::text[],
dismiss_user_ids integer[] DEFAULT '{}'::integer[],
dismissible boolean DEFAULT false,
platforms text[] DEFAULT '{}'::text[]
clients text[] DEFAULT '{}'::text[]
);


Expand Down
10 changes: 5 additions & 5 deletions schema/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -1852,7 +1852,7 @@
"placement": "mobile/home",
"locales": "{}",
"dismissible": true,
"platforms": "{}"
"clients": "{}"
},
{
"id": 2,
Expand All @@ -1864,7 +1864,7 @@
"placement": "mobile/home",
"locales": "{}",
"dismissible": true,
"platforms": "{}"
"clients": "{}"
},
{
"id": 3,
Expand All @@ -1876,7 +1876,7 @@
"placement": "mobile/home",
"locales": "{en-US,fr}",
"dismissible": true,
"platforms": "{}"
"clients": "{}"
},
{
"id": 4,
Expand All @@ -1888,7 +1888,7 @@
"placement": "mobile/home",
"locales": "{en}",
"dismissible": true,
"platforms": "{}"
"clients": "{}"
},
{
"id": 5,
Expand All @@ -1900,7 +1900,7 @@
"placement": "mobile/home",
"locales": "{}",
"dismissible": true,
"platforms": "{inat-ios,inat-android}"
"clients": "{inat-ios,inat-android}"
}
],
"comments": [
Expand Down
44 changes: 37 additions & 7 deletions test/integration/v2/announcements.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,27 +70,57 @@ describe( "Announcements", ( ) => {
.expect( 200, done );
} );

it( "returns announcements based on platform", function ( done ) {
it( "returns announcements based on client", function ( done ) {
const inatiOSAnnouncement = _.find(
fixtures.postgresql.announcements, a => a.platforms.match( /inat-ios/ )
fixtures.postgresql.announcements, a => a.clients.match( /inat-ios/ )
);
request( this.app ).get( "/v2/announcements?fields=all&platform=inat-ios" ).expect( res => {
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.platforms.includes( "inat-ios" ) || _.isEmpty( r.platforms )
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 platform not matching parameter", function ( done ) {
request( this.app ).get( "/v2/announcements?fields=all&platform=seek" ).expect( res => {
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.platforms ) ) ).to.be.true;
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 aeb0e7f

Please sign in to comment.