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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dronefly-discord
3
- Version: 0.2.0.dev0
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 (==0.5.0.dev0)
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
- def make_image_embed(taxon: Taxon, index, lang: str):
84
- formatter = TaxonFormatter(taxon, lang=lang, with_url=False)
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
- embed.set_image(url=taxon_photo.original_url)
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
- embed.description = "This taxon has no default photo."
110
+ description = "This taxon has no default photo."
107
111
  else:
108
112
  count = len(taxon.taxon_photos)
109
- embed.description = (
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.menus import BaseMenu as CoreBaseMenu
7
- from dronefly.core.formatters import TaxonFormatter, TaxonListFormatter
8
- from dronefly.core.menus import TaxonListSource as CoreTaxonListSource, ListPageSource
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._source.get_max_pages() - 1, interaction)
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
- # await self.view.show_checked_page(self.view.current_page + 1, interaction)
269
- pass
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
- # await self.view.show_checked_page(self.view.current_page + 1, interaction)
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
- # await self.view.show_checked_page(self.view.current_page + 1, interaction)
299
- pass
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
- # await self.view.show_checked_page(self.view.current_page + 1, interaction)
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, CoreBaseMenu):
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._source.get_page(self.current_page)
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._source.query_response.user:
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._source.get_page(page_number)
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._source.get_max_pages()
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._source = self._source.__class__(
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 TaxonMenu(DiscordBaseMenu, CoreBaseMenu):
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.user_button = UserButton(discord.ButtonStyle.grey, 0)
604
- self.query_user_button = QueryUserButton(discord.ButtonStyle.grey, 0)
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.add_item(self.user_button)
608
- self.add_item(self.query_user_button)
609
- self.add_item(self.taxonomy_button)
610
- self.add_item(self.stop_button)
611
-
612
- @property
613
- def source(self):
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._source.format_page)
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
- return {"embed": value, "content": None}
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
- return bool(self.ctx.inat_client.ctx.author.inat_user_id)
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="You are not authorized to interact with this.", ephemeral=True
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.dev0"
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 = "=0.5.0.dev0"
22
+ dronefly-core = {version = "^0.5.0.dev1", allow-prereleases = true}
24
23
 
25
- [tool.poetry.dev-dependencies]
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"