Skip to content

Commit e221f37

Browse files
Merge pull request #45 from pebble-dev/add_reporting_endpoint
Add flag endpoint
2 parents 483f323 + 4317058 commit e221f37

File tree

5 files changed

+110
-7
lines changed

5 files changed

+110
-7
lines changed

appstore/dev_portal_api.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from sqlalchemy.orm.exc import NoResultFound
77

88
from .utils import authed_request, demand_authed_request, get_uid
9-
from .models import LockerEntry, UserLike, db, App, Developer
9+
from .models import LockerEntry, UserLike, db, App, Developer, UserFlag
10+
from .discord import report_app_flag
1011
from .settings import config
1112

1213
parent_app = None
@@ -27,13 +28,14 @@ def me():
2728
rebble_id = me['rebble_id']
2829
added_ids = [x.app_id for x in LockerEntry.query.filter_by(user_id=rebble_id)]
2930
voted_ids = [x.app_id for x in UserLike.query.filter_by(user_id=rebble_id)]
31+
flagged_ids = [x.app_id for x in UserFlag.query.filter_by(user_id=rebble_id)]
3032
return jsonify({
3133
'users': [{
3234
'id': me['id'],
3335
'uid': me['uid'],
3436
'added_ids': added_ids,
3537
'voted_ids': voted_ids,
36-
'flagged_ids': [],
38+
'flagged_ids': flagged_ids,
3739
'applications': [],
3840
'name': me['name'],
3941
'href': request.url,
@@ -148,6 +150,35 @@ def remove_heart(app_id):
148150
algolia_index.partial_update_object({'objectID': app_id, 'hearts': app.hearts}, no_create=True)
149151
return 'ok'
150152

153+
@legacy_api.route('/applications/<app_id>/add_flag', methods=['POST'])
154+
def add_flag(app_id):
155+
uid = get_uid()
156+
157+
try:
158+
app = App.query.filter_by(id=app_id).one()
159+
flag = UserFlag(user_id=uid, app_id=app_id)
160+
db.session.add(flag)
161+
db.session.commit()
162+
report_app_flag(app.title, app.developer.name, app_id, app.app_uuid)
163+
except NoResultFound:
164+
abort(404)
165+
return
166+
except IntegrityError:
167+
return jsonify(error="already flagged",e="flag.exists"), 400
168+
return 'ok'
169+
170+
171+
@legacy_api.route('/applications/<app_id>/remove_flag', methods=['POST'])
172+
def remove_flag(app_id):
173+
uid = get_uid()
174+
try:
175+
flag = UserFlag.query.filter_by(app_id=app_id, user_id=uid).one()
176+
except NoResultFound:
177+
return ''
178+
db.session.delete(flag)
179+
db.session.commit()
180+
return 'ok'
181+
151182

152183
def init_app(app, url_prefix='/api/v0'):
153184
global parent_app

appstore/discord.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def announce_new_app(app, is_generated):
9393
"fields": request_fields
9494
}]
9595
}
96-
96+
9797
send_discord_webhook(request_data, is_generated)
9898

9999
def audit_log(operation, affected_app_uuid = None):
@@ -127,6 +127,39 @@ def audit_log(operation, affected_app_uuid = None):
127127

128128
send_admin_discord_webhook(request_data)
129129

130+
def report_app_flag(app_name, developer_name, app_id, affected_app_uuid = None):
131+
132+
if affected_app_uuid is not None:
133+
if config["TEST_APP_UUID"] is not None and config["TEST_APP_UUID"] == str(affected_app_uuid):
134+
return
135+
136+
request_fields = [{
137+
"name": "App",
138+
"value": app_name
139+
},
140+
{
141+
"name": "Developer",
142+
"value": developer_name
143+
}
144+
]
145+
146+
request_data = {
147+
"embeds": [{
148+
"title": f"New Flagged App Report 🚩",
149+
"color": int("0xFF4745", 0),
150+
"description": f"An end user has reported an app on the appstore from within a mobile app.",
151+
"thumbnail": {
152+
"url": "https://i.imgur.com/5f6rGQ9.png",
153+
"height": 80,
154+
"width": 80
155+
},
156+
"url": f"{config['APPSTORE_ROOT']}/application/{app_id}",
157+
"fields": request_fields
158+
}]
159+
}
160+
161+
send_admin_discord_webhook(request_data)
162+
130163
def send_discord_webhook(request_data, is_generated = False):
131164
if not is_generated:
132165
if config['DISCORD_HOOK_URL'] is not None:
@@ -135,7 +168,7 @@ def send_discord_webhook(request_data, is_generated = False):
135168
else:
136169
if config['DISCORD_GENERATED_HOOK_URL'] is not None:
137170
headers = {'Content-Type': 'application/json'}
138-
requests.post(config['DISCORD_GENERATED_HOOK_URL'], data=json.dumps(request_data), headers=headers)
171+
requests.post(config['DISCORD_GENERATED_HOOK_URL'], data=json.dumps(request_data), headers=headers)
139172

140173
def send_admin_discord_webhook(request_data):
141174
if config['DISCORD_ADMIN_HOOK_URL'] is not None:

appstore/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@ class UserLike(db.Model):
168168
app = db.relationship('App')
169169
db.Index('user_like_app_user_index', UserLike.app_id, UserLike.user_id, unique=True)
170170

171+
class UserFlag(db.Model):
172+
__tablename__ = "user_flags"
173+
user_id = db.Column(db.Integer(), primary_key=True, index=True)
174+
app_id = db.Column(db.String(24), db.ForeignKey('apps.id', ondelete='cascade'), primary_key=True, index=True)
175+
app = db.relationship('App')
176+
db.Index('user_flag_app_user_index', UserFlag.app_id, UserFlag.user_id, unique=True)
177+
171178
def init_app(app):
172179
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
173180
db.init_app(app)

appstore/utils.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ def jsonify_app(app: App, target_hw: str) -> dict:
136136
'share': f"{config['APPSTORE_ROOT']}/application/{app.id}",
137137
'add': 'https://a',
138138
'remove': 'https://b',
139-
'add_flag': 'https://c',
140-
'remove_flag': 'https://d',
139+
'add_flag': url_for('legacy_api.add_flag', app_id=app.id, _external=True),
140+
'remove_flag': url_for('legacy_api.remove_flag', app_id=app.id, _external=True),
141141
},
142142
'list_image': {
143143
'80x80': generate_image_url(app.icon_large, 80, 80, True),
@@ -209,14 +209,16 @@ def asset_fallback(collections: Dict[str, AssetCollection], target_hw='basalt')
209209
# and given that, produce the sanest possible result.
210210
# In particular, monochrome devices have colour fallbacks to reduce the chance of
211211
# ending up with round screenshots.
212+
# 13 Aug 25 - WM - Apparently we are getting a lot of requests with 'unknown' as the target_hw.
213+
# Anyone failing to identify will be presumed to be diorite.
212214
fallbacks = {
213215
'aplite': ['aplite', 'diorite', 'basalt'],
214216
'basalt': ['basalt', 'aplite'],
215217
'chalk': ['chalk', 'basalt'],
216218
'diorite': ['diorite', 'aplite', 'basalt'],
217219
'emery': ['emery', 'basalt', 'diorite', 'aplite']
218220
}
219-
fallback = fallbacks[target_hw]
221+
fallback = fallbacks[target_hw] if target_hw in fallbacks else fallbacks['diorite']
220222
for hw in fallback:
221223
if hw in collections:
222224
return collections[hw]
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Add user_reports table
2+
3+
Revision ID: ddb71f0a1c96
4+
Revises: c4e0470dc040
5+
Create Date: 2025-08-19 23:16:05.018578
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = 'ddb71f0a1c96'
14+
down_revision = 'c4e0470dc040'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
op.create_table(
21+
'user_flags',
22+
sa.Column('user_id', sa.Integer(), nullable=False),
23+
sa.Column('app_id', sa.String(length=24), nullable=False),
24+
sa.PrimaryKeyConstraint('app_id', 'user_id', name='user_flags_pkey')
25+
)
26+
27+
28+
def downgrade():
29+
op.drop_table('user_flags')
30+

0 commit comments

Comments
 (0)