Skip to content

Commit d7a5be3

Browse files
jwisehellcp
authored andcommitted
devportal: add support for posting to discourse topics
1 parent f19d613 commit d7a5be3

File tree

6 files changed

+138
-8
lines changed

6 files changed

+138
-8
lines changed

appstore/developer_portal_api.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
from .pbw import PBW, release_from_pbw
1818
from .s3 import upload_pbw, upload_asset, get_link_for_archive
1919
from .settings import config
20-
from .discord import announce_release, announce_new_app, audit_log
21-
20+
from .discord import audit_log
21+
from . import discord, discourse
2222

2323
parent_app = None
2424
devportal_api = Blueprint('devportal_api', __name__)
@@ -205,10 +205,16 @@ def submit_new_app():
205205
algolia_index.partial_update_objects([algolia_app(app_obj)], { 'createIfNotExists': True })
206206

207207
try:
208-
announce_new_app(app_obj, pbw.is_generated())
209-
except Exception:
208+
discord.announce_new_app(app_obj, pbw.is_generated())
209+
except Exception as e:
210210
# We don't want to fail just because Discord is being weird
211-
print("Discord is being weird")
211+
print("Discord is being weird: {repr(e)}")
212+
213+
try:
214+
discourse.announce_new_app(app_obj, pbw.is_generated())
215+
except Exception as e:
216+
# We don't want to fail just because Discourse is being weird
217+
print("Discourse is being weird: {repr(e)}")
212218

213219
return jsonify(success=True, id=app_obj.id)
214220

@@ -344,10 +350,16 @@ def submit_new_release(app_id):
344350
db.session.commit()
345351

346352
try:
347-
announce_release(app, release_new, pbw.is_generated())
348-
except Exception:
353+
discord.announce_release(app, release_new, pbw.is_generated())
354+
except Exception as e:
349355
# We don't want to fail just because Discord webhook is being weird
350-
print("Discord is being weird")
356+
print("Discord is being weird: {repr(e)}")
357+
358+
try:
359+
discourse.announce_release(app, release_new, pbw.is_generated())
360+
except Exception as e:
361+
# We don't want to fail just because Discourse webhook is being weird
362+
print(f"Discourse is being weird: {repr(e)}")
351363

352364
return jsonify(success=True)
353365

appstore/discourse.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from pydiscourse.client import DiscourseClient
2+
3+
from .settings import config
4+
from .models import App, db
5+
from .discord import random_party_emoji
6+
from .utils import get_app_description, generate_image_url
7+
8+
if config['DISCOURSE_API_KEY'] is None:
9+
_client = None
10+
else:
11+
_client = DiscourseClient(host=f"https://{config['DISCOURSE_HOST']}", api_username=config['DISCOURSE_USER'], api_key=config['DISCOURSE_API_KEY'])
12+
13+
def _md_quotify(text):
14+
return '\n'.join("> " + line for line in text.split('\n'))
15+
16+
def _create_or_post_to_topic(app, is_generated, text):
17+
if is_generated:
18+
# For now, we don't post about generated watchfaces. Maybe they
19+
# should go in their own topic later?
20+
return
21+
22+
if app.discourse_topic_id is -1:
23+
# We have manually set that we don't want a Discourse topic at all
24+
# for this app. Don't post at all.
25+
return
26+
27+
if app.discourse_topic_id is None:
28+
# This app doesn't have a topic of its own yet; create a new one,
29+
# and store it in the database.
30+
31+
if app.type == "watchapp":
32+
tags = ['pebble-app', 'watchapp', app.category.name]
33+
type_displayed = "Watchapp"
34+
else:
35+
tags = ['pebble-app', 'watchface']
36+
type_displayed = "Watchface"
37+
38+
# If any of this fails, someone can swallow the exception externally.
39+
rv = _client.create_post(text,
40+
category_id=config['DISCOURSE_SHOWCASE_TOPIC_ID'],
41+
title=f"{type_displayed}: {app.title} by {app.developer.name}",
42+
tags=tags)
43+
44+
App.query.filter_by(app_uuid=app.app_uuid).update({'discourse_topic_id': rv['topic_id']})
45+
db.session.commit()
46+
else:
47+
_client.create_post(text, category_id=config['DISCOURSE_SHOWCASE_TOPIC_ID'], topic_id=app.discourse_topic_id)
48+
49+
def announce_release(app, release, is_generated):
50+
_create_or_post_to_topic(app, is_generated, text=f"""
51+
# {random_party_emoji()} Update alert!
52+
53+
{app.developer.name} just released *version *{release.version}** of **{app.title}**!
54+
55+
[Go check it out!]({config['APPSTORE_ROOT']}/application/{app.id})
56+
57+
## Release notes
58+
59+
{_md_quotify(release.release_notes or "N/A")}
60+
61+
""")
62+
63+
def announce_new_app(app, is_generated):
64+
_create_or_post_to_topic(app, is_generated, text=f"""
65+
# {app.title} by {app.developer.name}
66+
67+
There's a new {app.type} on the Rebble App Store!
68+
69+
{app.developer.name} says:
70+
71+
{_md_quotify(get_app_description(app))}
72+
73+
[Go check it out in the App Store!]({config['APPSTORE_ROOT']}/application/{app.id})
74+
75+
![App icon]({generate_image_url(app.icon_large)})
76+
77+
*P.S.: I'm just a helpful robot that posted this. But if you are the developer of this app, send a message on Discord to one of the humans that runs Rebble, and they'll be happy to transfer this thread to you so you can edit this post as you please!*
78+
""")
79+
80+
def get_topic_url_for_app(app):
81+
if not _client or not app.discourse_topic_id or app.discourse_topic_id == -1:
82+
return None
83+
return f"https://{config['DISCOURSE_HOST']}/t/{app.discourse_topic_id}"

appstore/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class App(db.Model):
7878
visible = db.Column(db.Boolean, default=True, server_default='TRUE', nullable=False)
7979
timeline_token = db.Column(db.String, index=True)
8080
installs = db.Column(db.Integer, index=True)
81+
discourse_topic_id = db.Column(db.Integer)
8182

8283

8384
category_banner_apps = Table('category_banner_apps', db.Model.metadata,

appstore/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,8 @@
2828
'AWS_SECRET_KEY': os.environ.get('AWS_SECRET_KEY', None),
2929
'S3_ENDPOINT': os.environ.get('S3_ENDPOINT', None),
3030
'TEST_APP_UUID': os.environ.get('TEST_APP_UUID', None),
31+
'DISCOURSE_USER': os.environ.get('DISCOURSE_USER', 'annedroid'),
32+
'DISCOURSE_API_KEY': os.environ.get('DISCOURSE_API_KEY', None),
33+
'DISCOURSE_HOST': os.environ.get('DISCOURSE_HOST', f'forums.{domain_root}'),
34+
'DISCOURSE_SHOWCASE_TOPIC_ID': int(os.environ.get('DISCOURSE_SHOWCASE_TOPIC_ID', '3')),
3135
}

appstore/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import beeline
1515

16+
import appstore # break the circular dependency to import get_topic_url_for_app from discourse
1617
from .settings import config
1718
from appstore.models import App, AssetCollection, CompanionApp
1819

@@ -158,6 +159,7 @@ def jsonify_app(app: App, target_hw: str) -> dict:
158159
},
159160
'published_date': app.published_date,
160161
'visible': app.visible,
162+
'discourse_url': appstore.discourse.get_topic_url_for_app(app),
161163
}
162164
if release:
163165
result['latest_release'] = {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Add Discourse topic ID
2+
3+
Revision ID: 89e32c02be56
4+
Revises: c2c7be2e8835
5+
Create Date: 2025-12-03 09:42:17.001613
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '89e32c02be56'
14+
down_revision = 'c2c7be2e8835'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.add_column('apps', sa.Column('discourse_topic_id', sa.Integer(), nullable=True))
22+
# ### end Alembic commands ###
23+
24+
25+
def downgrade():
26+
# ### commands auto generated by Alembic - please adjust! ###
27+
op.drop_column('apps', 'discourse_topic_id')
28+
# ### end Alembic commands ###

0 commit comments

Comments
 (0)