Skip to content

Commit 202d58b

Browse files
authored
Merge pull request #256 from davidhozic/bug/252_direct_message
Fixes bugs: - #253 - #252
2 parents 7b372ce + 6a6b330 commit 202d58b

File tree

9 files changed

+156
-38
lines changed

9 files changed

+156
-38
lines changed

src/daf/client.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def __init__(self,
109109
connector = ProxyConnector.from_url(proxy)
110110

111111
self._client = discord.Client(intents=intents, connector=connector)
112+
self._deleted = False
112113
misc._write_attr_once(self, "_update_sem", asyncio.Semaphore(2))
113114

114115
def __eq__(self, other):
@@ -131,6 +132,19 @@ def running(self) -> bool:
131132
"""
132133
return self._running
133134

135+
@property
136+
def deleted(self) -> bool:
137+
"""
138+
Returns
139+
-----------
140+
True
141+
The object is no longer in the framework and should no longer
142+
be used.
143+
False
144+
Object is in the framework in normal operation.
145+
"""
146+
return self._deleted
147+
134148
@property
135149
def servers(self):
136150
"""
@@ -143,6 +157,16 @@ def servers(self):
143157
def client(self) -> discord.Client:
144158
"Returns the API wrapper client"
145159
return self._client
160+
161+
def _delete(self):
162+
"""
163+
Sets the internal _deleted flag to True,
164+
indicating the object should not be used.
165+
"""
166+
self._deleted = True
167+
for server in self.servers:
168+
server._delete()
169+
146170

147171
async def initialize(self):
148172
"""
@@ -212,6 +236,7 @@ def remove_server(self, server: Union[guild.GUILD, guild.USER, guild.AutoGUILD])
212236
``server`` is not in the shilling list.
213237
"""
214238
if isinstance(server, guild._BaseGUILD):
239+
server._delete()
215240
self._servers.remove(server)
216241
else:
217242
self._autoguilds.remove(server)
@@ -242,17 +267,19 @@ def get_server(self, snowflake: Union[int, discord.Guild, discord.User, discord.
242267

243268
return None
244269

245-
async def close(self):
270+
async def _close(self):
246271
"""
247272
Signals the tasks of this account to finish and
248273
waits for them.
249274
"""
250275
trace(f"Logging out of {self.client.user.display_name}...")
251276
self._running = False
277+
self._delete()
252278
for exc in await asyncio.gather(self.loop_task, return_exceptions=True):
253279
if exc is not None:
254280
trace(f"Exception occurred in main task for account {self.client.user.display_name} (Token: {self._token[:TOKEN_MAX_PRINT_LEN]})",
255281
TraceLEVELS.ERROR, exc)
282+
256283
await self._client.close()
257284

258285
async def _loop(self):

src/daf/core.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,11 @@ async def add_object(obj, snowflake=None):
257257

258258
@typechecked
259259
@misc.doc_category("Dynamic mod.")
260-
def remove_object(snowflake: Union[guild._BaseGUILD, message.BaseMESSAGE, guild.AutoGUILD, client.ACCOUNT]) -> None:
260+
async def remove_object(snowflake: Union[guild._BaseGUILD, message.BaseMESSAGE, guild.AutoGUILD, client.ACCOUNT]) -> None:
261261
"""
262+
.. versionchanged:: v2.4.1
263+
Turned async for fix bug of missing functionality
264+
262265
.. versionchanged:: v2.4
263266
| Now accepts client.ACCOUNT.
264267
| Removed support for ``int`` and for API wrapper (PyCord) objects.
@@ -289,6 +292,10 @@ def remove_object(snowflake: Union[guild._BaseGUILD, message.BaseMESSAGE, guild.
289292
for account in GLOBALS.accounts:
290293
if snowflake in account.servers:
291294
account.remove_server(snowflake)
295+
296+
elif isinstance(snowflake, client.ACCOUNT):
297+
await snowflake._close()
298+
GLOBALS.accounts.remove(snowflake)
292299

293300

294301
@typechecked
@@ -370,7 +377,7 @@ def _shutdown_clean(loop: asyncio.AbstractEventLoop) -> None:
370377
The loop to stop.
371378
"""
372379
for account in GLOBALS.accounts:
373-
loop.run_until_complete(account.close())
380+
loop.run_until_complete(account._close())
374381

375382

376383
@typechecked

src/daf/guild.py

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,19 @@ def __init__(self,
8080
def __repr__(self) -> str:
8181
return f"{type(self).__name__}(discord={self._apiobject})"
8282

83+
@property
84+
def deleted(self) -> bool:
85+
"""
86+
Returns
87+
-----------
88+
True
89+
The object is no longer in the framework and should no longer
90+
be used.
91+
False
92+
Object is in the framework in normal operation.
93+
"""
94+
return self._deleted
95+
8396
@property
8497
def messages(self) -> List[BaseMESSAGE]:
8598
"""
@@ -142,6 +155,8 @@ def _delete(self):
142155
Sets the internal _deleted flag to True.
143156
"""
144157
self._deleted = True
158+
for message in self._messages:
159+
message._delete()
145160

146161
@typechecked
147162
async def add_message(self, message: BaseMESSAGE):
@@ -183,6 +198,7 @@ def remove_message(self, message: BaseMESSAGE):
183198
ValueError
184199
Raised when the message is not present in the list.
185200
"""
201+
message._delete()
186202
self._messages.remove(message)
187203

188204
async def initialize(self, parent: Any, getter: Callable) -> None:
@@ -251,14 +267,18 @@ async def update(self, init_options={}, **kwargs):
251267
raise NotImplementedError
252268

253269
@misc._async_safe("update_semaphore", 1)
254-
async def _advertise(self):
270+
async def _advertise(self) -> List[Coroutine]:
255271
"""
256-
Main coroutine responsible for sending all the messages to this specific guild,
272+
Common to all messages, function responsible for sending all the messages to this specific guild,
257273
it is called from the core module's advertiser task.
274+
275+
Returns
276+
-----------
277+
List[Coroutine]
278+
List of coroutines that will call message._send() method.
258279
"""
259280
to_await = []
260281
to_remove = []
261-
guild_ctx = self.generate_log_context()
262282
for message in self._messages:
263283
if message._check_state():
264284
to_remove.append(message)
@@ -272,13 +292,7 @@ async def _advertise(self):
272292
for message in to_remove:
273293
self.remove_message(message)
274294

275-
# Await coroutines outside the main loop to prevent list modification (by user)
276-
# while iterating, this way even if the user removes the message, it will still be shilled
277-
# but no exceptions will be raised when trying to remove the message.
278-
for coro in to_await:
279-
message_ctx = await coro
280-
if self.logging and message_ctx is not None:
281-
await logging.save_log(guild_ctx, message_ctx)
295+
return to_await
282296

283297
def generate_log_context(self) -> Dict[str, Union[str, int]]:
284298
"""
@@ -364,6 +378,22 @@ async def initialize(self, parent: Any) -> None:
364378
Raised from .add_message(message_object) method.
365379
"""
366380
return await super().initialize(parent, parent.client.get_guild)
381+
382+
async def _advertise(self) -> None:
383+
"""
384+
Implementation specific _advertise method.
385+
Same as super()._advertise(), except it removes other DirectMESSAGE
386+
instances in case of them got a forbidden request.
387+
"""
388+
to_await = await super()._advertise()
389+
guild_ctx = self.generate_log_context()
390+
# Await coroutines outside the main loop to prevent list modification (by user)
391+
# while iterating, this way even if the user removes the message, it will still be shilled
392+
# but no exceptions will be raised when trying to remove the message.
393+
for coro in to_await:
394+
message_ctx = await coro
395+
if self.logging and message_ctx is not None:
396+
await logging.save_log(guild_ctx, message_ctx)
367397

368398
@misc._async_safe("update_semaphore", 2) # Take 2 since 2 tasks share access
369399
async def update(self, init_options={}, **kwargs):
@@ -428,6 +458,7 @@ class USER(_BaseGUILD):
428458
"""
429459
__slots__ = (
430460
"update_semaphore",
461+
"_panic"
431462
)
432463

433464
@typechecked
@@ -437,6 +468,7 @@ def __init__(self,
437468
logging: Optional[bool] = False,
438469
remove_after: Optional[Union[timedelta, datetime]]=None) -> None:
439470
super().__init__(snowflake, messages, logging, remove_after)
471+
self._panic = False # Set to True whenever message sends detected insufficient permissions
440472
misc._write_attr_once(self, "update_semaphore", asyncio.Semaphore(2)) # Only allows re-referencing this attribute once
441473

442474
def _check_state(self) -> bool:
@@ -450,7 +482,7 @@ def _check_state(self) -> bool:
450482
False
451483
The user is in proper state, do not delete.
452484
"""
453-
return super()._check_state()
485+
return self._panic or super()._check_state()
454486

455487
async def initialize(self, parent: Any):
456488
"""
@@ -465,6 +497,29 @@ async def initialize(self, parent: Any):
465497
"""
466498
return await super().initialize(parent, parent.client.get_or_fetch_user)
467499

500+
501+
async def _advertise(self) -> None:
502+
"""
503+
Implementation specific _advertise method.
504+
Same as super()._advertise(), except it removes other DirectMESSAGE
505+
instances in case of them got a forbidden request.
506+
"""
507+
to_await = await super()._advertise()
508+
guild_ctx = self.generate_log_context()
509+
# Await coroutines outside the main loop to prevent list modification (by user)
510+
# while iterating, this way even if the user removes the message, it will still be shilled
511+
# but no exceptions will be raised when trying to remove the message.
512+
for coro in to_await:
513+
message_ctx, panic = await coro
514+
if self.logging and message_ctx is not None:
515+
await logging.save_log(guild_ctx, message_ctx)
516+
517+
# panic means that the message send resulted in a forbidden error
518+
# signaling all other messages should be removed without send
519+
if panic:
520+
self._panic = True
521+
break
522+
468523
@misc._async_safe("update_semaphore", 2)
469524
async def update(self, init_options={}, **kwargs):
470525
"""
@@ -603,6 +658,19 @@ def created_at(self) -> datetime:
603658
Returns the datetime of when the object has been created.
604659
"""
605660
return self._created_at
661+
662+
@property
663+
def deleted(self) -> bool:
664+
"""
665+
Returns
666+
-----------
667+
True
668+
The object is no longer in the framework and should no longer
669+
be used.
670+
False
671+
Object is in the framework in normal operation.
672+
"""
673+
return self._deleted
606674

607675
def _delete(self):
608676
"""

src/daf/message/base.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ class BaseMESSAGE:
7777
"update_semaphore",
7878
"parent",
7979
"remove_after",
80-
"_created_at"
80+
"_created_at",
81+
"_deleted"
8182
)
8283

8384
@typechecked
@@ -126,11 +127,12 @@ def __init__(self,
126127
self._created_at = datetime.now()
127128
self._data = data
128129
self._fbcdata = isinstance(data, _FunctionBaseCLASS)
130+
self._deleted = False
129131
# Attributes created with this function will not be re-referenced to a different object
130132
# if the function is called again, ensuring safety (.update_method)
131133
misc._write_attr_once(self, "update_semaphore", asyncio.Semaphore(1))
132134
# For comparing copies of the object (prevents .update from overwriting)
133-
misc._write_attr_once(self, "_id", id(self))
135+
misc._write_attr_once(self, "_id", id(self))
134136

135137
def __repr__(self) -> str:
136138
return f"{type(self).__name__}(data={self._data})"
@@ -174,6 +176,26 @@ def created_at(self) -> datetime:
174176
"Returns the datetime of when the object was created"
175177
return self._created_at
176178

179+
@property
180+
def deleted(self) -> bool:
181+
"""
182+
Returns
183+
-----------
184+
True
185+
The object is no longer in the framework and should no longer
186+
be used.
187+
False
188+
Object is in the framework in normal operation.
189+
"""
190+
return self._deleted
191+
192+
def _delete(self):
193+
"""
194+
Sets the internal _deleted flag to True,
195+
indicating the object should not be used.
196+
"""
197+
self._deleted = True
198+
177199
def _check_state(self) -> bool:
178200
"""
179201
Checks if the message is ready to be deleted.

src/daf/message/text_based.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -716,15 +716,6 @@ async def _handle_error(self, ex: Exception) -> bool:
716716
if ex.code == 10008: # Unknown message
717717
self.previous_message = None
718718
handled = True
719-
elif ex.status == 403 or ex.code in {50007, 10001, 10003}:
720-
# Not permitted to send messages to that user!
721-
# Remove all messages to prevent an account ban
722-
for m in self.parent.messages:
723-
if m in self.parent.messages:
724-
self.parent.remove_message(m)
725-
726-
if ex.status in {400, 403}: # Bad Request
727-
await asyncio.sleep(RLIM_USER_WAIT_TIME) # To avoid triggering self-bot detection
728719

729720
return handled
730721

@@ -772,19 +763,22 @@ async def _send(self) -> Union[dict, None]:
772763
773764
Returns
774765
----------
775-
Union[Dict, None]
776-
Returns a dictionary generated by the ``generate_log_context`` method or the None object if message wasn't ready to be sent (:ref:`data_function` returned None or an invalid type)
777-
778-
This is then passed to :ref:`GUILD`._generate_log method.
766+
Tuple[Union[dict, None], bool]
767+
Returns a tuple of logging context and bool
768+
variable that signals the upper layer all other messages should be removed
769+
due to a forbidden error which automatically causes other messages to fail
770+
and increases risk of getting a user account banned.
779771
"""
780772
# Parse data from the data parameter
781773
data_to_send = await self._get_data()
774+
context, panic = None, False
782775
if any(data_to_send.values()):
783776
context = await self._send_channel(**data_to_send)
784777
self._update_state()
785-
return self.generate_log_context(context, **data_to_send)
778+
panic = ("reason" in context and context["reason"].status in {400, 403})
779+
context = self.generate_log_context(context, **data_to_send)
786780

787-
return None
781+
return context, panic
788782

789783
@typechecked
790784
@misc._async_safe("update_semaphore")

testing/test_autogen.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ async def test_autoguild(guilds, accounts):
3030
assert guild_exclude not in found, "AutoGUILD included the guild that matches exclude pattern."
3131
finally:
3232
if auto_guild is not None:
33-
daf.remove_object(auto_guild)
33+
await daf.remove_object(auto_guild)
3434

3535
@pytest.mark.asyncio
3636
async def test_autochannel(guilds, channels, accounts):
@@ -77,7 +77,7 @@ async def test_autochannel(guilds, channels, accounts):
7777
await auto_channel2.update()
7878
finally:
7979
if daf_guild is not None:
80-
daf.remove_object(daf_guild)
80+
await daf.remove_object(daf_guild)
8181

8282

8383

0 commit comments

Comments
 (0)