funpaybotengine 0.3.5__tar.gz → 0.4.0__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.
Files changed (119) hide show
  1. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/PKG-INFO +6 -6
  2. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/examples/with_redis_storage.py +1 -1
  3. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/client/bot.py +2 -2
  4. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/dispatching/events/base.py +25 -5
  5. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/dispatching/events/builtin_events.py +27 -27
  6. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_offer_fields.py +1 -1
  7. funpaybotengine-0.4.0/funpaybotengine/runner/config.py +116 -0
  8. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/runner/runner.py +40 -10
  9. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/messages.py +0 -1
  10. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/pyproject.toml +6 -6
  11. funpaybotengine-0.4.0/temp.py +0 -0
  12. funpaybotengine-0.3.5/comments.json +0 -1
  13. funpaybotengine-0.3.5/funpaybotengine/runner/config.py +0 -49
  14. funpaybotengine-0.3.5/html.html +0 -209
  15. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/.github/CODEOWNERS +0 -0
  16. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/.gitignore +0 -0
  17. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/.pre-commit-config.yaml +0 -0
  18. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/LICENSE +0 -0
  19. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/Makefile +0 -0
  20. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/README.md +0 -0
  21. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/examples/basic.py +0 -0
  22. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/examples/temp.py +0 -0
  23. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/__init__.py +0 -0
  24. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/base.py +0 -0
  25. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/client/__init__.py +0 -0
  26. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/client/default_hooks.py +0 -0
  27. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/client/session/__init__.py +0 -0
  28. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/client/session/aiohttp_session.py +0 -0
  29. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/client/session/base.py +0 -0
  30. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/client/session/http_methods.py +0 -0
  31. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/dispatching/__init__.py +0 -0
  32. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/dispatching/events/__init__.py +0 -0
  33. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/dispatching/filters/__init__.py +0 -0
  34. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/dispatching/filters/base.py +0 -0
  35. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/dispatching/filters/message_filters.py +0 -0
  36. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/dispatching/filters/order_filters.py +0 -0
  37. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/dispatching/handlers/__init__.py +0 -0
  38. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/dispatching/handlers/handler_manager.py +0 -0
  39. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/dispatching/routers/__init__.py +0 -0
  40. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/dispatching/routers/base.py +0 -0
  41. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/dispatching/routers/dispatcher.py +0 -0
  42. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/exceptions/__init__.py +0 -0
  43. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/exceptions/action_exceptions.py +0 -0
  44. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/exceptions/base.py +0 -0
  45. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/exceptions/bot_exceptions.py +0 -0
  46. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/exceptions/message_check_errors.py +0 -0
  47. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/exceptions/runner_exceptions.py +0 -0
  48. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/exceptions/session_exceptions.py +0 -0
  49. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/loggers.py +0 -0
  50. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/__init__.py +0 -0
  51. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/base.py +0 -0
  52. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/calc_chips.py +0 -0
  53. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/calc_lots.py +0 -0
  54. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/check_banned.py +0 -0
  55. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/delete_review.py +0 -0
  56. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_2fa_status.py +0 -0
  57. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_chat_history.py +0 -0
  58. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_chat_page.py +0 -0
  59. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_main_page.py +0 -0
  60. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_my_chips_page.py +0 -0
  61. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_my_offers_page.py +0 -0
  62. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_offer_page.py +0 -0
  63. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_order_page.py +0 -0
  64. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_profile_page.py +0 -0
  65. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_purchases.py +0 -0
  66. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_reviews.py +0 -0
  67. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_sales.py +0 -0
  68. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_settings_page.py +0 -0
  69. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_subcategory_page.py +0 -0
  70. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_telegram_connect_url.py +0 -0
  71. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_transactions.py +0 -0
  72. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/get_transactions_page.py +0 -0
  73. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/logout.py +0 -0
  74. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/mute_chat.py +0 -0
  75. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/raise_offers.py +0 -0
  76. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/refund.py +0 -0
  77. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/review.py +0 -0
  78. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/runner_request.py +0 -0
  79. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/save_offer_fields.py +0 -0
  80. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/set_offers_hidden.py +0 -0
  81. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/update_notice_channel.py +0 -0
  82. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/upload_avatar.py +0 -0
  83. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/methods/upload_image.py +0 -0
  84. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/py.typed +0 -0
  85. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/runner/__init__.py +0 -0
  86. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/runner/event_collector.py +0 -0
  87. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/storage/__init__.py +0 -0
  88. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/storage/base.py +0 -0
  89. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/storage/inmemory.py +0 -0
  90. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/storage/redis.py +0 -0
  91. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/__init__.py +0 -0
  92. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/base.py +0 -0
  93. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/calc.py +0 -0
  94. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/categories.py +0 -0
  95. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/chat.py +0 -0
  96. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/common.py +0 -0
  97. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/common_page_elements.py +0 -0
  98. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/enums.py +0 -0
  99. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/finances.py +0 -0
  100. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/offers.py +0 -0
  101. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/orders.py +0 -0
  102. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/pages/__init__.py +0 -0
  103. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/pages/base.py +0 -0
  104. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/pages/chat_page.py +0 -0
  105. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/pages/main_page.py +0 -0
  106. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/pages/my_chips_page.py +0 -0
  107. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/pages/my_offers_page.py +0 -0
  108. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/pages/offer_page.py +0 -0
  109. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/pages/order_page.py +0 -0
  110. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/pages/profile_page.py +0 -0
  111. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/pages/settings_page.py +0 -0
  112. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/pages/subcategory_page.py +0 -0
  113. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/pages/transactions_page.py +0 -0
  114. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/requests/__init__.py +0 -0
  115. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/requests/runner.py +0 -0
  116. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/reviews.py +0 -0
  117. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/settings.py +0 -0
  118. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/types/updates.py +0 -0
  119. {funpaybotengine-0.3.5 → funpaybotengine-0.4.0}/funpaybotengine/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: funpaybotengine
3
- Version: 0.3.5
3
+ Version: 0.4.0
4
4
  Summary: Framework for creating FunPay bots.
5
5
  Project-URL: Telegram, https://t.me/funpay_hub
6
6
  Project-URL: Issues, https://github.com/funpayhub/funpaybotengine/issues
@@ -18,11 +18,11 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: 3.14
19
19
  Classifier: Programming Language :: Python :: 3.15
20
20
  Requires-Python: >=3.10
21
- Requires-Dist: aiohttp-socks~=0.10.1
22
- Requires-Dist: aiohttp~=3.13.3
23
- Requires-Dist: eventry<0.4.0,>=0.3.3
24
- Requires-Dist: funpayparsers<1.0.0,>=0.5.13
25
- Requires-Dist: pydantic~=2.11.7
21
+ Requires-Dist: aiohttp-socks>=0.11.0
22
+ Requires-Dist: aiohttp<3.14,>=3.13.3
23
+ Requires-Dist: eventry<1.0.0,>=0.3.6
24
+ Requires-Dist: funpayparsers<1.0.0,>=0.5.14
25
+ Requires-Dist: pydantic<2.13,>=2.12
26
26
  Provides-Extra: dev
27
27
  Requires-Dist: mypy>=1.10.1; extra == 'dev'
28
28
  Requires-Dist: pre-commit>=4.5.1; extra == 'dev'
@@ -46,7 +46,7 @@ from contextlib import suppress
46
46
  from redis.asyncio import Redis
47
47
  from funpaybotengine import Bot, Dispatcher
48
48
  from funpaybotengine.types import Message
49
- from funpaybotengine.storage import RedisStorage
49
+ from funpaybotengine.storage.redis import RedisStorage
50
50
 
51
51
 
52
52
  # It is recommended to use more reliable data storage means, some kind of relational databases
@@ -308,7 +308,7 @@ class Bot:
308
308
  if category is None:
309
309
  raise ValueError(f'Category with ID {category_id} not found.')
310
310
  subcategory_ids = tuple(
311
- i.id for i in category.subcategories if i.type is SubcategoryType.COMMON
311
+ i.id for i in category.subcategories if i.type is SubcategoryType.OFFERS
312
312
  )
313
313
  return (
314
314
  await RaiseOffers(
@@ -763,7 +763,7 @@ class Bot:
763
763
  elif isinstance(subcategory, Subcategory):
764
764
  t, i = subcategory.type, subcategory.id
765
765
  elif isinstance(offer_id, int):
766
- t, i = SubcategoryType.COMMON, 1
766
+ t, i = SubcategoryType.OFFERS, 1
767
767
  else:
768
768
  raise ValueError(
769
769
  f'Invalid subcategory input: '
@@ -1,7 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
 
4
- __all__ = ('Event', 'RunnerEvent', 'BotEngineEvent', 'ExceptionEvent')
4
+ __all__ = (
5
+ 'Event',
6
+ 'RunnerEvent',
7
+ 'BotEngineEvent',
8
+ 'ExceptionEvent',
9
+ 'BotUnauthenticatedEvent',
10
+ 'BotAuthenticatedEvent',
11
+ )
5
12
 
6
13
  from typing import Any, Generic, TypeVar
7
14
  from types import MappingProxyType
@@ -16,7 +23,7 @@ EventObject = TypeVar('EventObject')
16
23
 
17
24
 
18
25
  # Not inheriting from ExtendedEvent, cz __iter__ conflict is pydantic BaseModel.
19
- class Event(EventryEvent, BindableObject, Generic[EventObject], name='event'):
26
+ class Event(EventryEvent, BindableObject, Generic[EventObject], event_name='event'):
20
27
  model_config = {
21
28
  'arbitrary_types_allowed': True,
22
29
  }
@@ -66,7 +73,7 @@ class Event(EventryEvent, BindableObject, Generic[EventObject], name='event'):
66
73
  return MappingProxyType(self._data)
67
74
 
68
75
 
69
- class RunnerEvent(Event[EventObject], name='runner'):
76
+ class RunnerEvent(Event[EventObject], event_name='runner'):
70
77
  tag: str | None = Field(frozen=True)
71
78
 
72
79
  @property
@@ -76,10 +83,10 @@ class RunnerEvent(Event[EventObject], name='runner'):
76
83
  return injection
77
84
 
78
85
 
79
- class BotEngineEvent(Event[EventObject], name='funpaybotengine'): ...
86
+ class BotEngineEvent(Event[EventObject], event_name='funpaybotengine'): ...
80
87
 
81
88
 
82
- class ExceptionEvent(BotEngineEvent[Exception], name='error'):
89
+ class ExceptionEvent(BotEngineEvent[Exception], event_name='error'):
83
90
  event: Event[Any] = Field(frozen=True)
84
91
 
85
92
  @property
@@ -87,3 +94,16 @@ class ExceptionEvent(BotEngineEvent[Exception], name='error'):
87
94
  injection = super().event_context_injection
88
95
  injection.update({'on_event': self.event, 'exception': self.object})
89
96
  return injection
97
+
98
+
99
+ class BotUnauthenticatedEvent(BotEngineEvent[float], event_name='unauthorized'):
100
+ delay: float
101
+
102
+ @property
103
+ def event_context_injection(self) -> dict[str, Any]:
104
+ injection = super().event_context_injection
105
+ injection.update({'delay': self.delay})
106
+ return injection
107
+
108
+
109
+ class BotAuthenticatedEvent(BotEngineEvent[None], event_name='authorized'): ...
@@ -43,7 +43,7 @@ from .base import RunnerEvent
43
43
  from ...types.pages import OrderPage
44
44
 
45
45
 
46
- class ChatChangedEvent(RunnerEvent[PrivateChatPreview], name='chat_changed'):
46
+ class ChatChangedEvent(RunnerEvent[PrivateChatPreview], event_name='chat_changed'):
47
47
  previous: PrivateChatPreview | None = None
48
48
 
49
49
  @property
@@ -57,7 +57,7 @@ class ChatChangedEvent(RunnerEvent[PrivateChatPreview], name='chat_changed'):
57
57
  }
58
58
 
59
59
 
60
- class NewMessageEvent(RunnerEvent[Message], name='new_message'):
60
+ class NewMessageEvent(RunnerEvent[Message], event_name='new_message'):
61
61
  @property
62
62
  def message(self) -> Message:
63
63
  return self.object
@@ -69,7 +69,7 @@ class NewMessageEvent(RunnerEvent[Message], name='new_message'):
69
69
  }
70
70
 
71
71
 
72
- class FromMessageEvent(NewMessageEvent, name='__from_message__'):
72
+ class FromMessageEvent(NewMessageEvent, event_name='__from_message__'):
73
73
  related_new_message_event: NewMessageEvent
74
74
 
75
75
  @property
@@ -79,14 +79,14 @@ class FromMessageEvent(NewMessageEvent, name='__from_message__'):
79
79
  return val
80
80
 
81
81
 
82
- class OrderEvent(FromMessageEvent, name='__order_event__'):
82
+ class OrderEvent(FromMessageEvent, event_name='__order_event__'):
83
83
  _order_preview: OrderPreview | None = PrivateAttr(default=None)
84
84
 
85
85
  async def get_order_preview(self, update: bool = False) -> OrderPreview:
86
86
  raise NotImplementedError
87
87
 
88
88
 
89
- class SaleEvent(OrderEvent, name='__sale_event__'):
89
+ class SaleEvent(OrderEvent, event_name='__sale_event__'):
90
90
  async def get_order_preview(self, update: bool = False) -> OrderPreview:
91
91
  if self._order_preview is not None and not update:
92
92
  return self._order_preview
@@ -95,7 +95,7 @@ class SaleEvent(OrderEvent, name='__sale_event__'):
95
95
  return orders.orders[0]
96
96
 
97
97
 
98
- class PurchaseEvent(OrderEvent, name='__purchase_event__'):
98
+ class PurchaseEvent(OrderEvent, event_name='__purchase_event__'):
99
99
  async def get_order_preview(self, update: bool = False) -> OrderPreview:
100
100
  if self._order_preview is not None and not update:
101
101
  return self._order_preview
@@ -106,55 +106,55 @@ class PurchaseEvent(OrderEvent, name='__purchase_event__'):
106
106
  return orders.orders[0]
107
107
 
108
108
 
109
- class NewSaleEvent(SaleEvent, name='new_sale'):
109
+ class NewSaleEvent(SaleEvent, event_name='new_sale'):
110
110
  related_auto_message_events: list[NewMessageEvent] = Field(default_factory=list)
111
111
 
112
112
 
113
- class SaleStatusChangedEvent(SaleEvent, name='sale_status_changed'):
113
+ class SaleStatusChangedEvent(SaleEvent, event_name='sale_status_changed'):
114
114
  previous: OrderPreview | None = None
115
115
 
116
116
 
117
- class SaleClosedEvent(SaleStatusChangedEvent, name='sale_closed'): ...
117
+ class SaleClosedEvent(SaleStatusChangedEvent, event_name='sale_closed'): ...
118
118
 
119
119
 
120
- class SaleClosedByAdminEvent(SaleClosedEvent, name='sale_closed_by_admin'): ...
120
+ class SaleClosedByAdminEvent(SaleClosedEvent, event_name='sale_closed_by_admin'): ...
121
121
 
122
122
 
123
- class SaleRefundedEvent(SaleStatusChangedEvent, name='sale_refunded'): ...
123
+ class SaleRefundedEvent(SaleStatusChangedEvent, event_name='sale_refunded'): ...
124
124
 
125
125
 
126
- class SalePartiallyRefundedEvent(SaleRefundedEvent, name='sale_partially_refunded'): ...
126
+ class SalePartiallyRefundedEvent(SaleRefundedEvent, event_name='sale_partially_refunded'): ...
127
127
 
128
128
 
129
- class SaleReopenedEvent(SaleStatusChangedEvent, name='sale_reopened'): ...
129
+ class SaleReopenedEvent(SaleStatusChangedEvent, event_name='sale_reopened'): ...
130
130
 
131
131
 
132
- class NewPurchaseEvent(PurchaseEvent, name='new_purchase'):
132
+ class NewPurchaseEvent(PurchaseEvent, event_name='new_purchase'):
133
133
  related_auto_message_events: list[NewMessageEvent] = Field(default_factory=list)
134
134
 
135
135
 
136
- class PurchaseStatusChangedEvent(PurchaseEvent, name='purchase_status_changed'):
136
+ class PurchaseStatusChangedEvent(PurchaseEvent, event_name='purchase_status_changed'):
137
137
  previous: OrderPreview | None = None
138
138
 
139
139
 
140
- class PurchaseClosedEvent(PurchaseStatusChangedEvent, name='purchase_closed'): ...
140
+ class PurchaseClosedEvent(PurchaseStatusChangedEvent, event_name='purchase_closed'): ...
141
141
 
142
142
 
143
- class PurchaseClosedByAdminEvent(PurchaseClosedEvent, name='purchase_closed_by_admin'): ...
143
+ class PurchaseClosedByAdminEvent(PurchaseClosedEvent, event_name='purchase_closed_by_admin'): ...
144
144
 
145
145
 
146
- class PurchaseRefundedEvent(PurchaseStatusChangedEvent, name='purchase_refunded'): ...
146
+ class PurchaseRefundedEvent(PurchaseStatusChangedEvent, event_name='purchase_refunded'): ...
147
147
 
148
148
 
149
149
  class PurchasePartiallyRefundedEvent(
150
- PurchaseRefundedEvent, name='purchase_partially_refunded'
150
+ PurchaseRefundedEvent, event_name='purchase_partially_refunded'
151
151
  ): ...
152
152
 
153
153
 
154
- class PurchaseReopenedEvent(PurchaseStatusChangedEvent, name='purchase_reopened'): ...
154
+ class PurchaseReopenedEvent(PurchaseStatusChangedEvent, event_name='purchase_reopened'): ...
155
155
 
156
156
 
157
- class ReviewEvent(FromMessageEvent, name='__review_event__'):
157
+ class ReviewEvent(FromMessageEvent, event_name='__review_event__'):
158
158
  _order_page: OrderPage | None = PrivateAttr(default=None)
159
159
 
160
160
  async def get_order_page(self, update: bool = False) -> OrderPage:
@@ -169,19 +169,19 @@ class ReviewEvent(FromMessageEvent, name='__review_event__'):
169
169
  return order_page.review
170
170
 
171
171
 
172
- class NewReviewEvent(ReviewEvent, name='new_review'): ...
172
+ class NewReviewEvent(ReviewEvent, event_name='new_review'): ...
173
173
 
174
174
 
175
- class ReviewChangedEvent(ReviewEvent, name='review_changed'): ...
175
+ class ReviewChangedEvent(ReviewEvent, event_name='review_changed'): ...
176
176
 
177
177
 
178
- class ReviewDeletedEvent(ReviewEvent, name='review_deleted'): ...
178
+ class ReviewDeletedEvent(ReviewEvent, event_name='review_deleted'): ...
179
179
 
180
180
 
181
- class NewReviewResponseEvent(ReviewEvent, name='new_review_response'): ...
181
+ class NewReviewResponseEvent(ReviewEvent, event_name='new_review_response'): ...
182
182
 
183
183
 
184
- class ReviewResponseChangedEvent(ReviewEvent, name='review_response_changed'): ...
184
+ class ReviewResponseChangedEvent(ReviewEvent, event_name='review_response_changed'): ...
185
185
 
186
186
 
187
- class ReviewResponseDeletedEvent(ReviewEvent, name='review_response_deleted'): ...
187
+ class ReviewResponseDeletedEvent(ReviewEvent, event_name='review_response_deleted'): ...
@@ -34,7 +34,7 @@ class GetOfferFields(FunPayMethod[OfferFields], BaseModel):
34
34
  offer_id: int | None = None,
35
35
  locale: Language | None = None,
36
36
  ):
37
- if subcategory_type is SubcategoryType.COMMON:
37
+ if subcategory_type is SubcategoryType.OFFERS:
38
38
  url = 'lots/offerEdit'
39
39
  data = {'offer': offer_id} if offer_id is not None else {'node': subcategory_id}
40
40
  else:
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ __all__ = ('RunnerConfig', 'BackoffConfig', 'Backoff')
5
+
6
+
7
+ import asyncio
8
+ from typing import Literal
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class BackoffConfig:
14
+ min_delay: float = 6.0
15
+ max_delay: float = 18.0
16
+ factor: float = 1.3
17
+
18
+ def __post_init__(self) -> None:
19
+ if self.max_delay <= self.min_delay:
20
+ raise ValueError('`max_delay` must be greater than `min_delay`.')
21
+ if self.factor <= 1:
22
+ raise ValueError('`factor` must be greater than 1')
23
+
24
+
25
+ @dataclass
26
+ class RunnerConfig:
27
+ interval: int | float = 4.0
28
+ """
29
+ Interval between updates requests.
30
+ Must be more than ``0``.
31
+
32
+ Defaults to ``4.0``.
33
+ """
34
+
35
+ discover_sales: bool = True
36
+ """Whether to discover new sales or not."""
37
+
38
+ discover_purchases: bool = True
39
+ """Whether to discover new purchases or not."""
40
+
41
+ keep_unread: bool = False
42
+ """
43
+ Whether to preserve the unread status of chats when getting updates.
44
+
45
+ If ``False`` (default), a batch method is used: up to 10 chats can be requested in a single
46
+ request, but they will be marked as read.
47
+
48
+ If ``True``, only one chat can be fetched per request to keep it unread.
49
+ With high incoming message volume this leads to many requests and may cause
50
+ ``429 Too Many Requests`` errors.
51
+ """
52
+
53
+ on_unauthenticated_error_policy: Literal['ignore', 'event', 'stop'] = 'ignore'
54
+ """
55
+ What to do when an `BotUnauthenticatedError` occurred during fetching updates process?
56
+
57
+ - ``ignore``: ignore the error and continue fetching updates.
58
+ - ``event``: yield an error event and continue fetching updates.
59
+ - ``stop``: stop fetching updates, exit a function.
60
+
61
+ Defaults to ``ignore``.
62
+ """
63
+
64
+ backoff_config: BackoffConfig = BackoffConfig()
65
+
66
+
67
+ class Backoff:
68
+ def __init__(self, config: BackoffConfig) -> None:
69
+ self.config = config
70
+ self._next_delay = config.min_delay
71
+ self._current_delay = 0.0
72
+ self._counter = 0
73
+
74
+ def __iter__(self) -> Backoff:
75
+ return self
76
+
77
+ @property
78
+ def min_delay(self) -> float:
79
+ return self.config.min_delay
80
+
81
+ @property
82
+ def max_delay(self) -> float:
83
+ return self.config.max_delay
84
+
85
+ @property
86
+ def factor(self) -> float:
87
+ return self.config.factor
88
+
89
+ @property
90
+ def next_delay(self) -> float:
91
+ return self._next_delay
92
+
93
+ @property
94
+ def current_delay(self) -> float:
95
+ return self._current_delay
96
+
97
+ @property
98
+ def counter(self) -> int:
99
+ return self._counter
100
+
101
+ async def asleep(self) -> None:
102
+ await asyncio.sleep(next(self))
103
+
104
+ def _calculate_next(self, value: float) -> float:
105
+ return min(value * self.factor, self.max_delay)
106
+
107
+ def __next__(self) -> float:
108
+ self._current_delay = self._next_delay
109
+ self._next_delay = self._calculate_next(self._next_delay)
110
+ self._counter += 1
111
+ return self._current_delay
112
+
113
+ def reset(self) -> None:
114
+ self._current_delay = 0.0
115
+ self._counter = 0
116
+ self._next_delay = self.min_delay
@@ -14,13 +14,15 @@ from collections.abc import Iterator, AsyncGenerator
14
14
  from funpaybotengine.loggers import runner_logger
15
15
  from funpaybotengine.exceptions import UnauthorizedError, BotUnauthenticatedError
16
16
  from funpaybotengine.storage.base import Storage
17
- from funpaybotengine.runner.config import RunnerConfig
18
- from funpaybotengine.runner.event_collector import EventCollector
19
- from funpaybotengine.dispatching.events.base import RunnerEvent
17
+ from funpaybotengine.dispatching.events import BotAuthenticatedEvent, BotUnauthenticatedEvent
18
+
19
+ from .config import Backoff, RunnerConfig
20
+ from .event_collector import EventCollector
20
21
 
21
22
 
22
23
  if TYPE_CHECKING:
23
24
  from funpaybotengine.client.bot import Bot
25
+ from funpaybotengine.dispatching.events import RunnerEvent, BotEngineEvent
24
26
 
25
27
 
26
28
  @dataclass
@@ -64,34 +66,62 @@ class Runner:
64
66
  config,
65
67
  session_storage=session_storage,
66
68
  )
69
+ backoff = Backoff(config.backoff_config)
67
70
 
68
71
  await collector.init_chats()
69
72
 
73
+ sleep_time: float | None = None
70
74
  while True:
71
75
  start = time.time()
76
+ result: list[RunnerEvent[Any] | BotEngineEvent[Any]] = []
72
77
 
73
78
  try:
74
79
  result = await collector.get_events()
80
+ if backoff.counter:
81
+ backoff.reset()
82
+ runner_logger.info('Connection established. Continuing collecting events.')
83
+ if config.on_unauthenticated_error_policy == 'event':
84
+ result.insert(0, BotAuthenticatedEvent(object=None))
85
+
75
86
  except (BotUnauthenticatedError, UnauthorizedError) as e:
76
87
  runner_logger.warning(
77
88
  'Bot is unauthenticated (%s). Executing current policy %r.',
78
89
  e.__class__.__name__,
79
90
  config.on_unauthenticated_error_policy,
80
91
  )
81
- if config.on_unauthenticated_error_policy == 'event':
82
- ...
92
+
93
+ if config.on_unauthenticated_error_policy in ['event', 'ignore']:
94
+ next(backoff)
95
+ sleep_time = backoff.current_delay
96
+ runner_logger.warning(
97
+ 'Current attempt: %d. Delay: %f.', backoff.counter, backoff.current_delay
98
+ )
99
+ if backoff.counter == 1 and config.on_unauthenticated_error_policy == 'event':
100
+ result = [
101
+ BotUnauthenticatedEvent(object=None, delay=backoff.current_delay)
102
+ ]
103
+
83
104
  elif config.on_unauthenticated_error_policy == 'stop':
84
105
  return
85
- elif config.on_unauthenticated_error_policy == 'stop+event':
86
- return # todo yield event
87
- await _sleep(start, config.interval)
88
- continue
106
+ except Exception:
107
+ next(backoff)
108
+ runner_logger.error(
109
+ 'Failed to collect events. Current attempt: %d. Delay: %f.',
110
+ backoff.counter,
111
+ backoff.current_delay,
112
+ exc_info=True,
113
+ )
114
+ sleep_time = backoff.current_delay
89
115
 
90
116
  events_stack = EventsStack(events=tuple(result))
91
117
  for i in events_stack:
92
118
  yield i, events_stack
93
119
 
94
- await _sleep(start, config.interval)
120
+ if sleep_time:
121
+ await asyncio.sleep(sleep_time)
122
+ sleep_time = None
123
+ else:
124
+ await _sleep(start, config.interval)
95
125
 
96
126
 
97
127
  async def _sleep(start_time: int | float, interval: int | float) -> None:
@@ -216,7 +216,6 @@ class Message(FunPayObject, BaseModel):
216
216
 
217
217
  return info.context.get('chat_id') if info.context else None
218
218
 
219
-
220
219
  @field_validator('chat_name', mode='before')
221
220
  @classmethod
222
221
  def _get_chat_name_from_context(cls, value: Any, info: ValidationInfo) -> Any:
@@ -1,16 +1,16 @@
1
1
  [project]
2
2
  name = "funpaybotengine"
3
- version = "0.3.5"
3
+ version = "0.4.0"
4
4
  description = "Framework for creating FunPay bots."
5
5
  requires-python = ">=3.10"
6
6
  readme = "README.md"
7
7
  license = "MIT"
8
8
  dependencies = [
9
- "aiohttp~=3.13.3",
10
- "aiohttp-socks~=0.10.1",
11
- "eventry>=0.3.3,<0.4.0",
12
- "funpayparsers>=0.5.13,<1.0.0",
13
- "pydantic~=2.11.7",
9
+ "aiohttp>=3.13.3,<3.14",
10
+ "aiohttp-socks>=0.11.0",
11
+ "eventry>=0.3.6,<1.0.0",
12
+ "funpayparsers>=0.5.14,<1.0.0",
13
+ "pydantic>=2.12,<2.13",
14
14
  ]
15
15
  authors = [
16
16
  {name = "Qvvonk", email="qvvonk@gmail.com"}
File without changes
@@ -1 +0,0 @@
1
- []
@@ -1,49 +0,0 @@
1
- from __future__ import annotations
2
-
3
-
4
- __all__ = ('RunnerConfig',)
5
-
6
-
7
- from typing import Literal
8
- from dataclasses import dataclass
9
-
10
-
11
- @dataclass
12
- class RunnerConfig:
13
- interval: int | float = 4.0
14
- """
15
- Interval between updates requests.
16
- Must be more than ``0``.
17
-
18
- Defaults to ``4.0``.
19
- """
20
-
21
- discover_sales: bool = True
22
- """Whether to discover new sales or not."""
23
-
24
- discover_purchases: bool = True
25
- """Whether to discover new purchases or not."""
26
-
27
- keep_unread: bool = False
28
- """
29
- Whether to preserve the unread status of chats when getting updates.
30
-
31
- If ``False`` (default), a batch method is used: up to 10 chats can be requested in a single
32
- request, but they will be marked as read.
33
-
34
- If ``True``, only one chat can be fetched per request to keep it unread.
35
- With high incoming message volume this leads to many requests and may cause
36
- ``429 Too Many Requests`` errors.
37
- """
38
-
39
- on_unauthenticated_error_policy: Literal['ignore', 'event', 'stop', 'stop+event'] = 'ignore'
40
- """
41
- What to do when an `BotUnauthenticatedError` occurred during fetching updates process?
42
-
43
- - ``ignore``: ignore the error and continue fetching updates.
44
- - ``event``: yield an error event and continue fetching updates.
45
- - ``stop``: stop fetching updates, exit a function.
46
- - ``stop+event``: yield an error event and stop fetching updates, exit a function.
47
-
48
- Defaults to ``ignore``.
49
- """
@@ -1,209 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="ru">
3
- <head>
4
- <meta charset="utf-8">
5
- <title>Редактирование предложения / FunPay</title><link href="https://funpay.com/700/css/main.css" rel="stylesheet" media="all">
6
- <script src="https://funpay.com/517/js/bundle.js"></script>
7
- <script src="https://funpay.com/517/js/lots.min.js"></script>
8
- <meta name="viewport" content="width=device-width, initial-scale=1">
9
- <link rel="alternate" hreflang="ru" href="https://funpay.com/lots/offerEdit?offer=6069527"><link rel="alternate" hreflang="uk" href="https://funpay.com/lots/offerEdit?offer=6069527"><link rel="alternate" hreflang="en" href="https://funpay.com/en/lots/offerEdit?offer=6069527"><link rel="alternate" hreflang="x-default" href="https://funpay.com/en/lots/offerEdit?offer=6069527"><script type="application/ld+json">[{"@context":"http:\/\/schema.org\/","@type":"WebSite","name":"FunPay","url":"https:\/\/funpay.com\/"}]</script><link rel="preload" href="/700/fonts/Graphik-Bold-Web.woff2" as="font" type="font/woff2" crossorigin><link rel="preload" href="/700/fonts/Graphik-Semibold-Web.woff2" as="font" type="font/woff2" crossorigin><link rel="manifest" href="/manifest.json">
10
- </head>
11
- <body class="with-corner-cd" data-app-data="{&quot;locale&quot;:&quot;ru&quot;,&quot;csrf-token&quot;:&quot;8g2f8avvrn4cd02m&quot;,&quot;userId&quot;:4067817,&quot;webpush&quot;:{&quot;app&quot;:&quot;C356D-32008&quot;,&quot;enabled&quot;:true,&quot;hwid-required&quot;:true}}">
12
- <!--suppress ALL -->
13
-
14
- <script>
15
- window.dataLayer = window.dataLayer || [];
16
- function gtag(){dataLayer.push(arguments);}
17
-
18
- gtag('consent', 'default', {"analytics_storage":"granted","ad_storage":"granted","ad_user_data":"granted","ad_personalization":"denied"});
19
- gtag('config', 'AW-11153904331');
20
-
21
- window.app.analytics.addEvents([{"event":"FunpayPageview","userId":"4067817","clientId":""}]);
22
- </script>
23
-
24
- <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
25
- new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
26
- j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
27
- 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
28
- })(window,document,'script','dataLayer','GTM-TWG83B');</script>
29
-
30
- <script type="text/javascript" >
31
- (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
32
- m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
33
- (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
34
-
35
- function initYM() {
36
- ym(36956765, "init", {
37
- clickmap: false,
38
- trackLinks: true,
39
- accurateTrackBounce: true,
40
- webvisor: false,
41
- ut: "noindex",
42
- ecommerce: "dataLayer"
43
- });
44
- }
45
- initYM();
46
- </script>
47
- <noscript><div><img src="https://mc.yandex.ru/watch/36956765?ut=noindex" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
48
- <div class="wrapper">
49
- <div class="wrapper-content">
50
- <div class="corner-cd-container"><div class="corner-cd"><img src="/img/circles/funpay_poke.jpg" alt=""></div></div>
51
- <header id="header">
52
- <div class="container">
53
- <div id="global-origin"></div>
54
- <nav class="navbar navbar-default">
55
- <div class="hide-navbar"></div>
56
- <div class="container-fluid">
57
- <div class="navbar-header">
58
- <button type="button" class="collapsed navbar-toggle navbar-toggle-logged" data-toggle="collapse" data-target="#navbar" aria-expanded="false">
59
- <span class="user-link-photo"><img src="https://sfunpay.com/s/avatar/fh/97/fh97o5taxakvmvm0r3e8.jpg" alt=""></span>
60
- <span class="caret"></span>
61
- </button>
62
- <a href="https://funpay.com/chat/" class="navbar-xs-icon icon-chat">
63
- <svg viewBox="0 0 26 26"><circle cx="13" cy="13" r="12"/><polygon points="26 26, 12 20, 20 12"/></svg>
64
- <span></span>
65
- </a>
66
- <a href="https://funpay.com/" class="navbar-brand">
67
- <img src="/img/layout/logo-funpay-white.svg" alt="FunPay" class="logo-white"/>
68
- <img src="/img/layout/logo-funpay.svg" alt="FunPay" class="logo-color"/>
69
- </a>
70
- <form action="https://funpay.com/games/promoFilter" method="post" class="navbar-form navbar-left dropdown promo-games-filter">
71
- <div class="form-group">
72
- <input type="text" name="query" class="form-control dropdown-toggle" data-toggle="dropdown" role="searchbox" placeholder="Поиск по 773 играм" autocomplete="off"/>
73
- <div class="dropdown-menu dropdown-autocomplete hidden"></div>
74
- </div>
75
- <button type="submit" class="btn btn-link"><i class="fa fa-search"></i></button>
76
- </form>
77
- </div>
78
- <div class="nav-container">
79
- <div class="collapse navbar-collapse no-transition" id="navbar">
80
- <ul class="nav navbar-nav">
81
- <li class="visible-xs visible-sm">
82
- <a href="https://funpay.com/users/4067817/" class="user-link-dropdown">
83
- <div class="user-link-name">EjikMLBB</div>
84
- <div class="user-link-desc">Профиль</div>
85
- </a>
86
- </li> <li class="dropdown"><a href="https://funpay.com/" class="menu-item-info dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Помощь <span class="caret"></span></a><ul class="dropdown-menu"><li><a href="https://funpay.com/trade/info" class="menu-item-trade_info">Правила</a></li>
87
- <li><a href="https://support.funpay.com/" class="menu-item-support" target="_blank">Центр помощи</a></li>
88
- <li><a href="https://support.funpay.com/tickets/new" class="menu-item-request" target="_blank">Отправить запрос</a></li>
89
- <li><a href="https://support.funpay.com/tickets" class="menu-item-requests" target="_blank">Мои запросы</a></li></ul></li> <li><a href="https://funpay.com/promo/lootery" class="menu-item-lootery">Лутерея</a></li> </ul>
90
- <ul class="nav navbar-nav navbar-right logged">
91
- <li><a href="https://funpay.com/orders/" class="menu-item-orders">Покупки <span class="badge badge-orders">1</span></a></li> <li><a href="https://funpay.com/orders/trade" class="menu-item-trade">Продажи <span class="badge badge-trade">2</span></a></li> <li><a href="https://funpay.com/chat/" class="menu-item-chat">Сообщения <span class="badge badge-chat hidden">0</span></a></li> <li><a href="https://funpay.com/account/balance" class="menu-item-balance">Финансы <span class="badge badge-balance">62 ₽</span></a></li> <li class="dropdown hidden-sm hidden-xs">
92
- <a href="javascript:void(0)" class="dropdown-toggle user-link" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
93
- <div class="user-link-photo"><img src="https://sfunpay.com/s/avatar/fh/97/fh97o5taxakvmvm0r3e8.jpg" alt=""/></div>
94
- <span class="caret"></span>
95
- </a>
96
- <ul class="dropdown-menu">
97
- <li>
98
- <a href="https://funpay.com/users/4067817/" class="user-link-dropdown">
99
- <div class="user-link-name">EjikMLBB</div>
100
- <div class="user-link-desc">Профиль</div>
101
- </a>
102
- </li> <li><a href="https://funpay.com/account/settings" class="menu-item-settings">Настройки</a></li><li class="dropdown">
103
- <a href="javascript:void(0)" class="dropdown-toggle menu-item-langs" data-toggle="dropdown">
104
- По-русски <i class="menu-icon menu-icon-lang-ru"></i> <span class="caret"></span>
105
- </a>
106
- <ul class="dropdown-menu">
107
- <li>
108
- <a href="https://funpay.com/lots/offerEdit?offer=6069527&amp;setlocale=en" class="menu-item-lang">
109
- English <i class="menu-icon menu-icon-lang-en"></i>
110
- </a>
111
- </li>
112
- </ul>
113
- </li><li class="dropdown">
114
- <a href="javascript:void(0)" class="dropdown-toggle menu-item-currencies" data-toggle="dropdown">
115
- Рубли <span class="caret"></span>
116
- </a>
117
- <ul class="dropdown-menu">
118
- <li>
119
- <a href="javascript:void(0)" data-cy="usd" class="user-cy-switcher menu-item-currency">
120
- Доллары </a>
121
- </li>
122
- <li>
123
- <a href="javascript:void(0)" data-cy="eur" class="user-cy-switcher menu-item-currency">
124
- Евро </a>
125
- </li>
126
- </ul>
127
- </li><li><a href="https://funpay.com/account/logout?token=a1fe429858772f68" class="menu-item-logout">Выйти</a></li> </ul>
128
- </li>
129
- <li class="visible-xs visible-sm"><a href="https://funpay.com/account/settings" class="menu-item-settings">Настройки</a></li><li class="visible-xs visible-sm dropdown">
130
- <a href="javascript:void(0)" class="dropdown-toggle menu-item-langs" data-toggle="dropdown">
131
- По-русски <i class="menu-icon menu-icon-lang-ru"></i> <span class="caret"></span>
132
- </a>
133
- <ul class="dropdown-menu">
134
- <li>
135
- <a href="https://funpay.com/lots/offerEdit?offer=6069527&amp;setlocale=en" class="menu-item-lang">
136
- English <i class="menu-icon menu-icon-lang-en"></i>
137
- </a>
138
- </li>
139
- </ul>
140
- </li><li class="visible-xs visible-sm dropdown">
141
- <a href="javascript:void(0)" class="dropdown-toggle menu-item-currencies" data-toggle="dropdown">
142
- Рубли <span class="caret"></span>
143
- </a>
144
- <ul class="dropdown-menu">
145
- <li>
146
- <a href="javascript:void(0)" data-cy="usd" class="user-cy-switcher menu-item-currency">
147
- Доллары </a>
148
- </li>
149
- <li>
150
- <a href="javascript:void(0)" data-cy="eur" class="user-cy-switcher menu-item-currency">
151
- Евро </a>
152
- </li>
153
- </ul>
154
- </li><li class="visible-xs visible-sm"><a href="https://funpay.com/account/logout?token=a1fe429858772f68" class="menu-item-logout">Выйти</a></li> </ul>
155
- </div>
156
- </div>
157
- </div>
158
- </nav>
159
- <div id="chat-notification" class="hidden" data-message="0">
160
- <audio class="loud"><source src="/audio/chat_loud.mp3" type="audio/mpeg"></audio>
161
- <audio class="quiet"><source src="/audio/chat_quiet.mp3" type="audio/mpeg"></audio>
162
- </div>
163
- <div id="live-counters" class="hidden" data-orders="5iz07iil" data-chat="pp9ddrdb"></div>
164
- </div>
165
- </header>
166
-
167
- <section id="content-body">
168
- <div class="search-fade"></div>
169
- <div id="content" class="content-lots content-lots-offeredit"><div class="container">
170
- <div class="row">
171
- <div class="col-md-2 col-sm-3">
172
- <div class="back-link">
173
- <a href="https://funpay.com/" class="js-back-link"><i class="fa fa-chevron-left"></i><span class="inside">Назад</span></a>
174
- </div>
175
- </div>
176
- <div class="col-md-10 col-sm-9">
177
- <div class="page-content">
178
- <h1 class="page-header page-header-no-hr">Редактирование предложения</h1>
179
- <p class="lead">Предложение не найдено.</p>
180
- </div>
181
- </div>
182
- </div>
183
- </div></div> </section>
184
- </div>
185
-
186
- <div class="wrapper-footer">
187
- <div class="container">
188
- <footer id="footer">
189
- <div class="footer-block footer-block-strings">
190
- <span>&copy; 2015&ndash;2026 FunPay</span>
191
- <a href="https://funpay.com/pages/privacyPolicy">Политика конфиденциальности</a> <a href="https://funpay.com/pages/cookiePolicy">Политика cookie</a> <a href="https://funpay.com/contacts.html" target="_blank" rel="nofollow">Контакты</a> </div>
192
- <div class="footer-block footer-block-social">
193
- <a class="btn btn-gray btn-round" href="https://t.me/+zC7jZ0ytESc1ZjUy" target="_blank" rel="nofollow">
194
- <svg width="25" height="20"><use xlink:href="/3/img/layout/sprites.svg#social-logo-telegram"></use></svg> </a>
195
- <a class="btn btn-gray btn-round" href="https://www.youtube.com/c/funpayofficial" target="_blank" rel="nofollow"><i class="fab fa-youtube"></i></a>
196
- <a class="btn btn-gray btn-round" href="https://vk.com/funpay" target="_blank" rel="nofollow"><i class="fab fa-vk"></i></a>
197
- <a class="btn btn-gray btn-round" href="https://www.facebook.com/funpay.official" target="_blank" rel="nofollow"><i class="fab fa-facebook-square"></i></a>
198
- </div>
199
- <div class="footer-block footer-block-als">
200
- <img src="/img/layout/als-logo.svg" alt="">
201
- <div>Задизайнено<br>в <a href="https://www.artlebedev.ru/funpay/" target="_blank">Студии Артемия<br>Лебедева</a></div>
202
- </div>
203
- </footer>
204
- </div>
205
- </div>
206
- </div>
207
-
208
- </body>
209
- </html>
File without changes