dronefly-discord 0.2.0.dev0__tar.gz → 0.2.0.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dronefly_discord-0.2.0.dev0 → dronefly_discord-0.2.0.dev1}/PKG-INFO +2 -3
- {dronefly_discord-0.2.0.dev0 → dronefly_discord-0.2.0.dev1}/dronefly/discord/embeds.py +31 -13
- {dronefly_discord-0.2.0.dev0 → dronefly_discord-0.2.0.dev1}/dronefly/discord/menus.py +355 -45
- {dronefly_discord-0.2.0.dev0 → dronefly_discord-0.2.0.dev1}/pyproject.toml +3 -4
- {dronefly_discord-0.2.0.dev0 → dronefly_discord-0.2.0.dev1}/LICENSE +0 -0
- {dronefly_discord-0.2.0.dev0 → dronefly_discord-0.2.0.dev1}/README.md +0 -0
- {dronefly_discord-0.2.0.dev0 → dronefly_discord-0.2.0.dev1}/dronefly/discord/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: dronefly-discord
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev1
|
|
4
4
|
Summary: Dronefly Discord library
|
|
5
5
|
License: AGPL-3.0-or-later
|
|
6
6
|
Author: Ben Armstrong
|
|
@@ -10,9 +10,8 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or l
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Requires-Dist: discord-py (>=2.3.1)
|
|
13
|
-
Requires-Dist: dronefly-core (
|
|
13
|
+
Requires-Dist: dronefly-core (>=0.5.0.dev1,<0.6.0)
|
|
14
14
|
Requires-Dist: inflect (>=5.3.0,<6.0.0)
|
|
15
|
-
Requires-Dist: pyinaturalist (>=0.20.0,<0.21.0)
|
|
16
15
|
Description-Content-Type: text/markdown
|
|
17
16
|
|
|
18
17
|
# Dronefly Discord
|
|
@@ -7,6 +7,7 @@ from pyinaturalist.models import IconPhoto, Taxon
|
|
|
7
7
|
from dronefly.core import formatters
|
|
8
8
|
from dronefly.core.formatters.constants import WWW_BASE_URL
|
|
9
9
|
from dronefly.core.formatters.generic import (
|
|
10
|
+
CountFormatter,
|
|
10
11
|
format_taxon_names,
|
|
11
12
|
TaxonFormatter,
|
|
12
13
|
)
|
|
@@ -62,6 +63,16 @@ def format_taxon_names_for_embed(*args, **kwargs):
|
|
|
62
63
|
return format_taxon_names(*args, **kwargs)
|
|
63
64
|
|
|
64
65
|
|
|
66
|
+
def make_count_embed(formatter: CountFormatter, description: str):
|
|
67
|
+
"""Make a count embed."""
|
|
68
|
+
embed = make_embed(
|
|
69
|
+
url=f"{formatter.source.url}",
|
|
70
|
+
title=f"Observations {formatter.source.query_response.obs_query_description()}",
|
|
71
|
+
description=description,
|
|
72
|
+
)
|
|
73
|
+
return embed
|
|
74
|
+
|
|
75
|
+
|
|
65
76
|
def make_embed(**kwargs):
|
|
66
77
|
"""Make a standard embed."""
|
|
67
78
|
return discord.Embed(color=EMBED_COLOR, **kwargs)
|
|
@@ -80,10 +91,8 @@ def make_taxa_embed(taxon: Taxon, formatter: TaxonFormatter, description: str):
|
|
|
80
91
|
return embed
|
|
81
92
|
|
|
82
93
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
title = formatter.format_title()
|
|
86
|
-
|
|
94
|
+
# TODO: migrate into lower level classes
|
|
95
|
+
def get_taxon_photo(taxon, index):
|
|
87
96
|
taxon_photo = None
|
|
88
97
|
if (
|
|
89
98
|
index == 1
|
|
@@ -93,21 +102,30 @@ def make_image_embed(taxon: Taxon, index, lang: str):
|
|
|
93
102
|
taxon_photo = taxon.default_photo
|
|
94
103
|
elif index >= 1 and index <= len(taxon.taxon_photos):
|
|
95
104
|
taxon_photo = taxon.taxon_photos[index - 1]
|
|
96
|
-
|
|
97
|
-
embed = make_embed(url=f"{WWW_BASE_URL}/taxa/{taxon.id}")
|
|
98
|
-
embed.title = title
|
|
99
|
-
|
|
105
|
+
description = ""
|
|
100
106
|
if taxon_photo:
|
|
101
|
-
|
|
102
|
-
embed.set_footer(text=taxon_photo.attribution)
|
|
103
|
-
embed.description = f"Photo {index} of {len(taxon.taxon_photos)}"
|
|
107
|
+
description = f"Photo {index} of {len(taxon.taxon_photos)}"
|
|
104
108
|
else:
|
|
105
109
|
if index == 1:
|
|
106
|
-
|
|
110
|
+
description = "This taxon has no default photo."
|
|
107
111
|
else:
|
|
108
112
|
count = len(taxon.taxon_photos)
|
|
109
|
-
|
|
113
|
+
description = (
|
|
110
114
|
f"Photo number {index} not found.\n"
|
|
111
115
|
f"Taxon has {count} {p.plural('photo', count)}."
|
|
112
116
|
)
|
|
117
|
+
return (taxon_photo, description)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def make_image_embed(taxon: Taxon, formatter: TaxonFormatter, index: int = 1):
|
|
121
|
+
title = formatter.format_title()
|
|
122
|
+
|
|
123
|
+
(taxon_photo, description) = get_taxon_photo(taxon, index)
|
|
124
|
+
formatter.image_description = description
|
|
125
|
+
embed = make_embed(url=f"{WWW_BASE_URL}/taxa/{taxon.id}")
|
|
126
|
+
embed.title = title
|
|
127
|
+
if taxon_photo:
|
|
128
|
+
embed.set_image(url=taxon_photo.original_url)
|
|
129
|
+
embed.set_footer(text=taxon_photo.attribution)
|
|
130
|
+
embed.description = formatter.format()
|
|
113
131
|
return embed
|
|
@@ -1,14 +1,25 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from math import floor
|
|
2
3
|
from typing import Any, Optional
|
|
3
4
|
|
|
4
5
|
import discord
|
|
5
6
|
from discord.ext import commands
|
|
6
|
-
from dronefly.core.
|
|
7
|
-
from dronefly.core.formatters import
|
|
8
|
-
from dronefly.core.menus import
|
|
7
|
+
from dronefly.core.clients.inat import iNatClient
|
|
8
|
+
from dronefly.core.formatters import TaxonListFormatter
|
|
9
|
+
from dronefly.core.menus import (
|
|
10
|
+
CountMenu as CoreCountMenu,
|
|
11
|
+
CountSource as CoreCountSource,
|
|
12
|
+
TaxonMenu as CoreTaxonMenu,
|
|
13
|
+
TaxonListMenu as CoreTaxonListMenu,
|
|
14
|
+
TaxonListSource as CoreTaxonListSource,
|
|
15
|
+
TaxonSource as CoreTaxonSource,
|
|
16
|
+
)
|
|
9
17
|
from pyinaturalist import ROOT_TAXON_ID, Taxon
|
|
18
|
+
from requests import HTTPError
|
|
10
19
|
|
|
11
|
-
from .embeds import make_embed
|
|
20
|
+
from .embeds import make_count_embed, make_embed, make_image_embed, make_taxa_embed
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
12
23
|
|
|
13
24
|
|
|
14
25
|
class TaxonListSource(CoreTaxonListSource):
|
|
@@ -84,7 +95,7 @@ class LastItemButton(discord.ui.Button):
|
|
|
84
95
|
self.emoji = "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}" # noqa: E501
|
|
85
96
|
|
|
86
97
|
async def callback(self, interaction: discord.Interaction):
|
|
87
|
-
await self.view.show_page(self.view.
|
|
98
|
+
await self.view.show_page(self.view.source.get_max_pages() - 1, interaction)
|
|
88
99
|
|
|
89
100
|
|
|
90
101
|
class FirstItemButton(discord.ui.Button):
|
|
@@ -244,11 +255,9 @@ class SelectTaxonListTaxon(discord.ui.Select):
|
|
|
244
255
|
class DiscordBaseMenu(discord.ui.View):
|
|
245
256
|
def __init__(
|
|
246
257
|
self,
|
|
247
|
-
source: ListPageSource,
|
|
248
258
|
timeout: int = 60,
|
|
249
259
|
**kwargs: Any,
|
|
250
260
|
) -> None:
|
|
251
|
-
self._source = source
|
|
252
261
|
super().__init__(
|
|
253
262
|
timeout=timeout,
|
|
254
263
|
)
|
|
@@ -265,8 +274,90 @@ class UserButton(discord.ui.Button):
|
|
|
265
274
|
self.emoji = "\N{BUST IN SILHOUETTE}"
|
|
266
275
|
|
|
267
276
|
async def callback(self, interaction: discord.Interaction):
|
|
268
|
-
|
|
269
|
-
|
|
277
|
+
view = self.view
|
|
278
|
+
inat_client = view.inat_client
|
|
279
|
+
await interaction.response.defer()
|
|
280
|
+
user = await (
|
|
281
|
+
await inat_client.users.from_dronefly_users([interaction.user])
|
|
282
|
+
).async_one()
|
|
283
|
+
await self.view.source.toggle_user_count(inat_client, user)
|
|
284
|
+
await self.view.show_page(interaction)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class QueryUserModal(discord.ui.Modal):
|
|
288
|
+
def __init__(self, view=None):
|
|
289
|
+
super().__init__(title="Add or remove a user")
|
|
290
|
+
self.view = view
|
|
291
|
+
self.discord_user = discord.ui.UserSelect()
|
|
292
|
+
self.user_select_label = discord.ui.Label(
|
|
293
|
+
text="Select a member to add/remove", component=self.discord_user
|
|
294
|
+
)
|
|
295
|
+
self.user_text = discord.ui.TextInput(required=False)
|
|
296
|
+
self.user_text_label = discord.ui.Label(
|
|
297
|
+
text="Or type an iNat username", component=self.user_text
|
|
298
|
+
)
|
|
299
|
+
self.add_item(self.user_select_label)
|
|
300
|
+
self.add_item(self.user_text_label)
|
|
301
|
+
|
|
302
|
+
async def on_submit(self, interaction: discord.Interaction):
|
|
303
|
+
try:
|
|
304
|
+
view = self.view
|
|
305
|
+
inat_client = view.inat_client
|
|
306
|
+
dronefly_config = view.dronefly_ctx.config
|
|
307
|
+
|
|
308
|
+
discord_user_values = self.discord_user.values
|
|
309
|
+
discord_user_value = None
|
|
310
|
+
user_text_value = self.user_text.value
|
|
311
|
+
|
|
312
|
+
if not (discord_user_values or user_text_value):
|
|
313
|
+
await interaction.response.send_message(
|
|
314
|
+
content="No member selected or iNat username typed.", ephemeral=True
|
|
315
|
+
)
|
|
316
|
+
return
|
|
317
|
+
elif discord_user_values and user_text_value:
|
|
318
|
+
await interaction.response.send_message(
|
|
319
|
+
content="Choose only a member or iNat username, not both.",
|
|
320
|
+
ephemeral=True,
|
|
321
|
+
)
|
|
322
|
+
return
|
|
323
|
+
user_for_member = None
|
|
324
|
+
user_for_text = None
|
|
325
|
+
inat_user_id = None
|
|
326
|
+
if discord_user_values:
|
|
327
|
+
discord_user_value = discord_user_values[0]
|
|
328
|
+
if not isinstance(discord_user_value, discord.Member):
|
|
329
|
+
discord_user_value = await commands.MemberConverter().convert(
|
|
330
|
+
self.view.ctx, discord_user_value
|
|
331
|
+
)
|
|
332
|
+
inat_user_id = await dronefly_config.user_id(discord_user_value)
|
|
333
|
+
if inat_user_id:
|
|
334
|
+
user_for_member = await inat_client.users.from_ids(
|
|
335
|
+
inat_user_id
|
|
336
|
+
).async_one()
|
|
337
|
+
if user_for_member:
|
|
338
|
+
await view.source.toggle_user_count(
|
|
339
|
+
inat_client, user_for_member
|
|
340
|
+
)
|
|
341
|
+
if user_text_value:
|
|
342
|
+
inat_user_id = await dronefly_config.user_id(user_text_value)
|
|
343
|
+
if inat_user_id:
|
|
344
|
+
user_for_text = await inat_client.users.from_ids(
|
|
345
|
+
inat_user_id
|
|
346
|
+
).async_one()
|
|
347
|
+
await view.source.toggle_user_count(inat_client, user_for_text)
|
|
348
|
+
except (HTTPError, LookupError):
|
|
349
|
+
pass
|
|
350
|
+
if user_for_member or user_for_text:
|
|
351
|
+
await view.show_page(interaction)
|
|
352
|
+
elif discord_user_value:
|
|
353
|
+
await interaction.response.send_message(
|
|
354
|
+
content="iNat user not known for that member.", ephemeral=True
|
|
355
|
+
)
|
|
356
|
+
else:
|
|
357
|
+
await interaction.response.send_message(
|
|
358
|
+
content="iNat user not found.", ephemeral=True
|
|
359
|
+
)
|
|
360
|
+
return
|
|
270
361
|
|
|
271
362
|
|
|
272
363
|
class QueryUserButton(discord.ui.Button):
|
|
@@ -280,8 +371,7 @@ class QueryUserButton(discord.ui.Button):
|
|
|
280
371
|
self.emoji = "\N{BUSTS IN SILHOUETTE}"
|
|
281
372
|
|
|
282
373
|
async def callback(self, interaction: discord.Interaction):
|
|
283
|
-
|
|
284
|
-
pass
|
|
374
|
+
await interaction.response.send_modal(QueryUserModal(view=self.view))
|
|
285
375
|
|
|
286
376
|
|
|
287
377
|
class HomePlaceButton(discord.ui.Button):
|
|
@@ -295,8 +385,63 @@ class HomePlaceButton(discord.ui.Button):
|
|
|
295
385
|
self.emoji = "\N{HOUSE BUILDING}"
|
|
296
386
|
|
|
297
387
|
async def callback(self, interaction: discord.Interaction):
|
|
298
|
-
|
|
299
|
-
|
|
388
|
+
view = self.view
|
|
389
|
+
inat_client = view.inat_client
|
|
390
|
+
dronefly_config = view.dronefly_ctx.config
|
|
391
|
+
|
|
392
|
+
await interaction.response.defer()
|
|
393
|
+
place_id = await dronefly_config.place_id("home", interaction.user)
|
|
394
|
+
place = None
|
|
395
|
+
if place_id:
|
|
396
|
+
place = await inat_client.places.from_ids(place_id).async_one()
|
|
397
|
+
if place:
|
|
398
|
+
await self.view.source.toggle_place_count(inat_client, place)
|
|
399
|
+
await self.view.show_page(interaction)
|
|
400
|
+
else:
|
|
401
|
+
await interaction.response.send_message(
|
|
402
|
+
"You have not set a home place", ephemeral=True
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class QueryPlaceModal(discord.ui.Modal):
|
|
407
|
+
def __init__(self, view=None):
|
|
408
|
+
super().__init__(title="Add or remove a place")
|
|
409
|
+
self.view = view
|
|
410
|
+
self.place_text = discord.ui.TextInput(required=True)
|
|
411
|
+
self.place_text_label = discord.ui.Label(
|
|
412
|
+
text="Type an iNat place or abbreviation", component=self.place_text
|
|
413
|
+
)
|
|
414
|
+
self.add_item(self.place_text_label)
|
|
415
|
+
|
|
416
|
+
async def on_submit(self, interaction: discord.Interaction):
|
|
417
|
+
try:
|
|
418
|
+
view = self.view
|
|
419
|
+
inat_client = view.inat_client
|
|
420
|
+
dronefly_config = view.dronefly_ctx.config
|
|
421
|
+
|
|
422
|
+
place_text_value = self.place_text.value
|
|
423
|
+
|
|
424
|
+
if place_text_value:
|
|
425
|
+
inat_place_id = await dronefly_config.place_id(place_text_value)
|
|
426
|
+
if inat_place_id:
|
|
427
|
+
place_for_text = await inat_client.places.from_ids(
|
|
428
|
+
inat_place_id
|
|
429
|
+
).async_one()
|
|
430
|
+
else:
|
|
431
|
+
places_for_text = await inat_client.places.autocomplete(
|
|
432
|
+
q=place_text_value, limit=1
|
|
433
|
+
).async_all()
|
|
434
|
+
if places_for_text:
|
|
435
|
+
place_for_text = places_for_text[0]
|
|
436
|
+
if place_for_text:
|
|
437
|
+
await view.source.toggle_place_count(inat_client, place_for_text)
|
|
438
|
+
await view.show_page(interaction)
|
|
439
|
+
return
|
|
440
|
+
except (HTTPError, LookupError):
|
|
441
|
+
pass
|
|
442
|
+
await interaction.response.send_message(
|
|
443
|
+
content="iNat place not found.", ephemeral=True
|
|
444
|
+
)
|
|
300
445
|
|
|
301
446
|
|
|
302
447
|
class QueryPlaceButton(discord.ui.Button):
|
|
@@ -310,8 +455,7 @@ class QueryPlaceButton(discord.ui.Button):
|
|
|
310
455
|
self.emoji = "\N{EARTH GLOBE EUROPE-AFRICA}"
|
|
311
456
|
|
|
312
457
|
async def callback(self, interaction: discord.Interaction):
|
|
313
|
-
|
|
314
|
-
pass
|
|
458
|
+
await interaction.response.send_modal(QueryPlaceModal(view=self.view))
|
|
315
459
|
|
|
316
460
|
|
|
317
461
|
class TaxonomyButton(discord.ui.Button):
|
|
@@ -330,14 +474,16 @@ class TaxonomyButton(discord.ui.Button):
|
|
|
330
474
|
await self.view.show_page(interaction)
|
|
331
475
|
|
|
332
476
|
|
|
333
|
-
class TaxonListMenu(DiscordBaseMenu,
|
|
477
|
+
class TaxonListMenu(DiscordBaseMenu, CoreTaxonListMenu):
|
|
334
478
|
def __init__(
|
|
335
479
|
self,
|
|
480
|
+
source: TaxonListSource,
|
|
336
481
|
cog: commands.Cog,
|
|
337
482
|
message: discord.Message = None,
|
|
338
483
|
**kwargs: Any,
|
|
339
484
|
) -> None:
|
|
340
485
|
super().__init__(**kwargs)
|
|
486
|
+
self.source = source
|
|
341
487
|
self.cog = cog
|
|
342
488
|
self.bot = None
|
|
343
489
|
self.message = message
|
|
@@ -363,10 +509,6 @@ class TaxonListMenu(DiscordBaseMenu, CoreBaseMenu):
|
|
|
363
509
|
self.add_item(self.forward_button)
|
|
364
510
|
self.add_item(self.last_item)
|
|
365
511
|
|
|
366
|
-
@property
|
|
367
|
-
def source(self):
|
|
368
|
-
return self._source
|
|
369
|
-
|
|
370
512
|
async def on_timeout(self):
|
|
371
513
|
await self.message.edit(view=None)
|
|
372
514
|
|
|
@@ -399,7 +541,7 @@ class TaxonListMenu(DiscordBaseMenu, CoreBaseMenu):
|
|
|
399
541
|
This implementation shows the first page of the source.
|
|
400
542
|
"""
|
|
401
543
|
self.ctx = ctx
|
|
402
|
-
page = await self.
|
|
544
|
+
page = await self.source.get_page(self.current_page)
|
|
403
545
|
kwargs = await self._get_kwargs_from_page(page)
|
|
404
546
|
if getattr(page[0], "descendant_obs_count", None):
|
|
405
547
|
# Source modifier buttons for life list:
|
|
@@ -411,7 +553,7 @@ class TaxonListMenu(DiscordBaseMenu, CoreBaseMenu):
|
|
|
411
553
|
self.add_item(self.per_rank_button)
|
|
412
554
|
self.add_item(self.root_button)
|
|
413
555
|
self.add_item(self.direct_button)
|
|
414
|
-
if self.
|
|
556
|
+
if self.source.query_response.user:
|
|
415
557
|
self.common_button = CommonButton(discord.ButtonStyle.grey, 1)
|
|
416
558
|
self.add_item(self.common_button)
|
|
417
559
|
self.select_taxon = SelectTaxonListTaxon(view=self, page=page, selected=0)
|
|
@@ -422,7 +564,7 @@ class TaxonListMenu(DiscordBaseMenu, CoreBaseMenu):
|
|
|
422
564
|
async def show_page(
|
|
423
565
|
self, page_number: int, interaction: discord.Interaction, selected: int = 0
|
|
424
566
|
):
|
|
425
|
-
page = await self.
|
|
567
|
+
page = await self.source.get_page(page_number)
|
|
426
568
|
self.current_page = page_number
|
|
427
569
|
self.ctx.selected = selected
|
|
428
570
|
kwargs = await self._get_kwargs_from_page(page)
|
|
@@ -435,7 +577,7 @@ class TaxonListMenu(DiscordBaseMenu, CoreBaseMenu):
|
|
|
435
577
|
async def show_checked_page(
|
|
436
578
|
self, page_number: int, interaction: discord.Interaction
|
|
437
579
|
) -> None:
|
|
438
|
-
max_pages = self.
|
|
580
|
+
max_pages = self.source.get_max_pages()
|
|
439
581
|
try:
|
|
440
582
|
if max_pages is None:
|
|
441
583
|
# If it doesn't give maximum pages, it cannot be checked
|
|
@@ -540,7 +682,7 @@ class TaxonListMenu(DiscordBaseMenu, CoreBaseMenu):
|
|
|
540
682
|
)
|
|
541
683
|
self._taxon_list_formatter = formatter
|
|
542
684
|
# Replace the source
|
|
543
|
-
self.
|
|
685
|
+
self.source = self.source.__class__(
|
|
544
686
|
taxon_list,
|
|
545
687
|
query_response,
|
|
546
688
|
formatter,
|
|
@@ -587,31 +729,182 @@ class TaxonListMenu(DiscordBaseMenu, CoreBaseMenu):
|
|
|
587
729
|
await self.show_page(page, interaction, selected)
|
|
588
730
|
|
|
589
731
|
|
|
590
|
-
class
|
|
732
|
+
class CountSource(CoreCountSource):
|
|
733
|
+
def format_page(self):
|
|
734
|
+
embed = make_count_embed(
|
|
735
|
+
formatter=self.formatter,
|
|
736
|
+
description=self.formatter.format(),
|
|
737
|
+
)
|
|
738
|
+
return embed
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
class CountMenu(DiscordBaseMenu, CoreCountMenu):
|
|
742
|
+
ctx: commands.Context = None
|
|
743
|
+
author: discord.Member = None
|
|
744
|
+
message: discord.Message = None
|
|
745
|
+
home_place_button: discord.Button = None
|
|
746
|
+
query_place_button: discord.Button = None
|
|
747
|
+
user_button: discord.Button = None
|
|
748
|
+
query_user_button: discord.Button = None
|
|
749
|
+
|
|
591
750
|
def __init__(
|
|
592
751
|
self,
|
|
593
752
|
cog: commands.Cog,
|
|
753
|
+
inat_client: iNatClient,
|
|
754
|
+
source: CountSource,
|
|
755
|
+
for_place: bool = None,
|
|
756
|
+
**kwargs: Any,
|
|
757
|
+
) -> None:
|
|
758
|
+
self.cog = cog
|
|
759
|
+
self.bot = self.cog.bot
|
|
760
|
+
self.inat_client = inat_client
|
|
761
|
+
self.source = source
|
|
762
|
+
self.for_place = for_place
|
|
763
|
+
if for_place:
|
|
764
|
+
self.home_place_button = HomePlaceButton(discord.ButtonStyle.grey, 0)
|
|
765
|
+
self.query_place_button = QueryPlaceButton(discord.ButtonStyle.grey, 0)
|
|
766
|
+
else:
|
|
767
|
+
self.user_button = UserButton(discord.ButtonStyle.grey, 0)
|
|
768
|
+
self.query_user_button = QueryUserButton(discord.ButtonStyle.grey, 0)
|
|
769
|
+
super().__init__(**kwargs)
|
|
770
|
+
|
|
771
|
+
async def on_timeout(self):
|
|
772
|
+
await self.message.edit(view=None)
|
|
773
|
+
|
|
774
|
+
async def start(self, ctx: commands.Context):
|
|
775
|
+
self.ctx = ctx
|
|
776
|
+
self.author = ctx.author
|
|
777
|
+
# await self.source._prepare_once()
|
|
778
|
+
# Place or user social buttons
|
|
779
|
+
if self.for_place:
|
|
780
|
+
self.add_item(self.home_place_button)
|
|
781
|
+
self.add_item(self.query_place_button)
|
|
782
|
+
else:
|
|
783
|
+
self.add_item(self.user_button)
|
|
784
|
+
self.add_item(self.query_user_button)
|
|
785
|
+
# Owner-only button to cancel the menu
|
|
786
|
+
self.stop_button = StopButton(discord.ButtonStyle.red, 0)
|
|
787
|
+
self.add_item(self.stop_button)
|
|
788
|
+
self.message = await self.send_initial_message(ctx)
|
|
789
|
+
|
|
790
|
+
async def _get_kwargs_from_page(self):
|
|
791
|
+
value = await discord.utils.maybe_coroutine(self.source.format_page)
|
|
792
|
+
if isinstance(value, dict):
|
|
793
|
+
return value
|
|
794
|
+
elif isinstance(value, str):
|
|
795
|
+
return {"content": value, "embed": None}
|
|
796
|
+
elif isinstance(value, discord.Embed):
|
|
797
|
+
return {"embed": value, "content": None}
|
|
798
|
+
|
|
799
|
+
async def send_initial_message(self, ctx: commands.Context):
|
|
800
|
+
"""|coro|
|
|
801
|
+
The default implementation of :meth:`Menu.send_initial_message`
|
|
802
|
+
for the interactive pagination session.
|
|
803
|
+
This implementation shows the first page of the source.
|
|
804
|
+
"""
|
|
805
|
+
self.ctx = ctx
|
|
806
|
+
kwargs = await self._get_kwargs_from_page()
|
|
807
|
+
self.message = await ctx.send(**kwargs, view=self)
|
|
808
|
+
return self.message
|
|
809
|
+
|
|
810
|
+
async def show_page(self, interaction: discord.Interaction):
|
|
811
|
+
self.current_page = 0
|
|
812
|
+
kwargs = await self._get_kwargs_from_page()
|
|
813
|
+
if interaction.response.is_done():
|
|
814
|
+
await interaction.edit_original_response(**kwargs, view=self)
|
|
815
|
+
else:
|
|
816
|
+
await interaction.response.edit_message(**kwargs, view=self)
|
|
817
|
+
|
|
818
|
+
async def interaction_check(self, interaction: discord.Interaction):
|
|
819
|
+
"""Allow owner and known iNat user interactions."""
|
|
820
|
+
# Only some buttons can be pressed by known users:
|
|
821
|
+
if interaction.data.get("custom_id") in [
|
|
822
|
+
"user",
|
|
823
|
+
"query_user",
|
|
824
|
+
"home_place",
|
|
825
|
+
"query_place",
|
|
826
|
+
# "taxonomy",
|
|
827
|
+
]:
|
|
828
|
+
dronefly_config = self.dronefly_ctx.config
|
|
829
|
+
try:
|
|
830
|
+
await dronefly_config.user_id(interaction.user)
|
|
831
|
+
except LookupError:
|
|
832
|
+
await interaction.response.send_message(
|
|
833
|
+
content="Your iNat account is not known here.", ephemeral=True
|
|
834
|
+
)
|
|
835
|
+
return False
|
|
836
|
+
return True
|
|
837
|
+
elif interaction.user.id not in (
|
|
838
|
+
*interaction.client.owner_ids,
|
|
839
|
+
getattr(self.author, "id", None),
|
|
840
|
+
):
|
|
841
|
+
# Other buttons can only be pressed by the owner:
|
|
842
|
+
await interaction.response.send_message(
|
|
843
|
+
content="Only the command owner can do this.", ephemeral=True
|
|
844
|
+
)
|
|
845
|
+
return False
|
|
846
|
+
return True
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
class TaxonSource(CoreTaxonSource):
|
|
850
|
+
def format_page(self):
|
|
851
|
+
# TODO: migrate photo concerns into taxon source & formatter:
|
|
852
|
+
if self.formatter.image_number is None:
|
|
853
|
+
embed = make_taxa_embed(
|
|
854
|
+
taxon=self.query_response.taxon,
|
|
855
|
+
formatter=self.formatter,
|
|
856
|
+
description=self.formatter.format(
|
|
857
|
+
with_title=False, with_ancestors=self.with_ancestors
|
|
858
|
+
),
|
|
859
|
+
)
|
|
860
|
+
else:
|
|
861
|
+
embed = make_image_embed(
|
|
862
|
+
taxon=self.query_response.taxon,
|
|
863
|
+
formatter=self.formatter,
|
|
864
|
+
index=self.formatter.image_number,
|
|
865
|
+
)
|
|
866
|
+
return embed
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
class TaxonMenu(DiscordBaseMenu, CoreTaxonMenu):
|
|
870
|
+
ctx: commands.Context = None
|
|
871
|
+
author: discord.Member = None
|
|
872
|
+
message: discord.Message = None
|
|
873
|
+
home_place_button: discord.Button = None
|
|
874
|
+
query_place_button: discord.Button = None
|
|
875
|
+
user_button: discord.Button = None
|
|
876
|
+
query_user_button: discord.Button = None
|
|
877
|
+
|
|
878
|
+
def __init__(
|
|
879
|
+
self,
|
|
880
|
+
inat_client: iNatClient,
|
|
881
|
+
source: TaxonSource,
|
|
882
|
+
cog: commands.Cog,
|
|
594
883
|
message: discord.Message = None,
|
|
884
|
+
for_place: bool = False,
|
|
885
|
+
image_number: int = None,
|
|
886
|
+
related_embed: discord.Embed = None,
|
|
595
887
|
**kwargs: Any,
|
|
596
888
|
) -> None:
|
|
597
889
|
super().__init__(**kwargs)
|
|
890
|
+
self.inat_client = inat_client
|
|
891
|
+
self.source = source
|
|
598
892
|
self.cog = cog
|
|
599
893
|
self.bot = None
|
|
600
894
|
self.message = message
|
|
601
895
|
self.ctx = None
|
|
602
896
|
self.author: Optional[discord.Member] = None
|
|
603
|
-
self.
|
|
604
|
-
self.
|
|
897
|
+
self.image_number = image_number
|
|
898
|
+
self.related_embed = related_embed
|
|
605
899
|
self.taxonomy_button = TaxonomyButton(discord.ButtonStyle.grey, 0)
|
|
606
900
|
self.stop_button = StopButton(discord.ButtonStyle.red, 0)
|
|
607
|
-
self.
|
|
608
|
-
self.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
return self._source
|
|
901
|
+
self.for_place = for_place
|
|
902
|
+
if self.for_place:
|
|
903
|
+
self.home_place_button = HomePlaceButton(discord.ButtonStyle.grey, 0)
|
|
904
|
+
self.query_place_button = QueryPlaceButton(discord.ButtonStyle.grey, 0)
|
|
905
|
+
else:
|
|
906
|
+
self.user_button = UserButton(discord.ButtonStyle.grey, 0)
|
|
907
|
+
self.query_user_button = QueryUserButton(discord.ButtonStyle.grey, 0)
|
|
615
908
|
|
|
616
909
|
async def on_timeout(self):
|
|
617
910
|
await self.message.edit(view=None)
|
|
@@ -621,16 +914,29 @@ class TaxonMenu(DiscordBaseMenu, CoreBaseMenu):
|
|
|
621
914
|
self.bot = self.cog.bot
|
|
622
915
|
self.author = ctx.author
|
|
623
916
|
# await self.source._prepare_once()
|
|
917
|
+
if self.for_place:
|
|
918
|
+
self.add_item(self.home_place_button)
|
|
919
|
+
self.add_item(self.query_place_button)
|
|
920
|
+
else:
|
|
921
|
+
self.add_item(self.user_button)
|
|
922
|
+
self.add_item(self.query_user_button)
|
|
923
|
+
if self.source.formatter.image_number is None:
|
|
924
|
+
self.add_item(self.taxonomy_button)
|
|
925
|
+
self.add_item(self.stop_button)
|
|
624
926
|
self.message = await self.send_initial_message(ctx)
|
|
625
927
|
|
|
626
928
|
async def _get_kwargs_from_page(self):
|
|
627
|
-
value = await discord.utils.maybe_coroutine(self.
|
|
929
|
+
value = await discord.utils.maybe_coroutine(self.source.format_page)
|
|
628
930
|
if isinstance(value, dict):
|
|
629
931
|
return value
|
|
630
932
|
elif isinstance(value, str):
|
|
631
933
|
return {"content": value, "embed": None}
|
|
632
934
|
elif isinstance(value, discord.Embed):
|
|
633
|
-
|
|
935
|
+
if self.related_embed:
|
|
936
|
+
embeds = [self.related_embed, value]
|
|
937
|
+
else:
|
|
938
|
+
embeds = [value]
|
|
939
|
+
return {"embeds": embeds, "content": None}
|
|
634
940
|
|
|
635
941
|
async def send_initial_message(self, ctx: commands.Context):
|
|
636
942
|
"""|coro|
|
|
@@ -659,20 +965,24 @@ class TaxonMenu(DiscordBaseMenu, CoreBaseMenu):
|
|
|
659
965
|
"query_user",
|
|
660
966
|
"home_place",
|
|
661
967
|
"query_place",
|
|
662
|
-
"taxonomy",
|
|
968
|
+
# "taxonomy",
|
|
663
969
|
]:
|
|
664
|
-
|
|
970
|
+
dronefly_config = self.dronefly_ctx.config
|
|
971
|
+
try:
|
|
972
|
+
await dronefly_config.user_id(interaction.user)
|
|
973
|
+
except LookupError:
|
|
974
|
+
await interaction.response.send_message(
|
|
975
|
+
content="Your iNat account is not known here.", ephemeral=True
|
|
976
|
+
)
|
|
977
|
+
return False
|
|
978
|
+
return True
|
|
665
979
|
elif interaction.user.id not in (
|
|
666
980
|
*interaction.client.owner_ids,
|
|
667
981
|
getattr(self.author, "id", None),
|
|
668
982
|
):
|
|
669
983
|
# Other buttons can only be pressed by the owner:
|
|
670
984
|
await interaction.response.send_message(
|
|
671
|
-
content="
|
|
985
|
+
content="Only the command owner can do this.", ephemeral=True
|
|
672
986
|
)
|
|
673
987
|
return False
|
|
674
988
|
return True
|
|
675
|
-
|
|
676
|
-
@property
|
|
677
|
-
def formatter(self) -> TaxonFormatter:
|
|
678
|
-
return self.source.formatter
|
|
@@ -6,7 +6,7 @@ exclude_dirs = ["tests"]
|
|
|
6
6
|
|
|
7
7
|
[tool.poetry]
|
|
8
8
|
name = "dronefly-discord"
|
|
9
|
-
version = "0.2.0.
|
|
9
|
+
version = "0.2.0.dev1"
|
|
10
10
|
description = "Dronefly Discord library"
|
|
11
11
|
authors = ["Ben Armstrong <synrg@debian.org>"]
|
|
12
12
|
license = "AGPL-3.0-or-later"
|
|
@@ -17,12 +17,11 @@ packages = [
|
|
|
17
17
|
|
|
18
18
|
[tool.poetry.dependencies]
|
|
19
19
|
python = ">=3.11,<3.12"
|
|
20
|
-
pyinaturalist = "^0.20.0"
|
|
21
20
|
inflect = "^5.3.0"
|
|
22
21
|
discord-py = ">=2.3.1"
|
|
23
|
-
dronefly-core = "
|
|
22
|
+
dronefly-core = {version = "^0.5.0.dev1", allow-prereleases = true}
|
|
24
23
|
|
|
25
|
-
[tool.poetry.dev
|
|
24
|
+
[tool.poetry.group.dev.dependencies]
|
|
26
25
|
black = "^24.3.0"
|
|
27
26
|
pytest = "^7.2.1"
|
|
28
27
|
pytest-mock = "^3.10.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|