-
Notifications
You must be signed in to change notification settings - Fork 3
/
EzMudae.py
474 lines (419 loc) · 16.6 KB
/
EzMudae.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
from __future__ import annotations
import enum
import asyncio
import time
import argparse
import parse
BIT_SIZE = 16
MUDA = 432610292342587392
EMOJI_FEMALE = "<:female:452463537508450304>"
EMOJI_MALE = "<:male:452470164529872899>"
EMOJI_KAKERA = "<:kakera:469835869059153940>"
class WaifuTypeError(TypeError):
"""
This is the exception to catch, if you're trying to parse every message as a waifu message
"""
pass
class Waifu:
"""
Represents a waifu from mudae.
A lot of the attributes will often be null since they either:
a. Are not applicable
b. Can't be read from the waifu message
c. Need to be fetched with the methods
Attributes
----------
mudae: discord.User
The mudae bot that created this waifu.
user: discord.Client
The client that's using this waifu.
message: discord.Message
The waifu message the waifu came from.
owner: discord.Member
The member whose harem the waifu belongs to.
creator: discord.Member
The member that rolled this waifu.
suitors: list[discord.Member]
A list of members who wished the waifu.
name: str
The name of the waifu.
series: str
The series the waifu belongs to.
kakera: int
The kakera value of the waifu.
key: int
The key level of the waifu.
claims: int
The claims rank of the waifu.
likes: int
The likes rank of the waifu
type: Waifu.Type
The type of the waifu.
image: str
URL of the image, that the waifu message had, when the object was created.
image_count: int
How many images the waifu has available in total.
image_index: int
Image index of the image attribute with respect to the avaliable images.
image_extra: int
How many extra images have been added to the waifu.
is_claimed: bool
If the waifu has been claimed yet.
is_girl: bool
If the waifu is female or both female and male.
Methods
-------
async fetch_extra()
Fills the suitor and creator attributes.
async await_claim()
Waits for a member to claim this waifu, then returns with that member.
"""
class Type(enum.Enum):
"""
Represents the different types of waifus.
Enums
-----
roll: 0
The waifu was rolled e.g. created with $w.
info: 1
The waifu came from the info command e.g. created with $im.
"""
roll = enum.auto()
info = enum.auto()
class Gender(enum.Enum):
"""
Represents the different genders of waifus.
Enums
-----
female: 0
The waifu is female.
male: 1
The waifu is male.
"""
female = enum.auto()
male = enum.auto()
def __init__(self, mudae, user, message):
self.mudae = mudae
self.message = message
self.user = user
self.suitors = [] # Needs to be fetched with fetch_extra
self.name = None # Name of the waifu, appears in the title of im and w
self.series = None # Series the waifu belongs to, appears in the description of im and w
self.kakera = None # Kakera value. Always appears in the description of w and optional in im
self.claims = None # Claims rank, appears in the description of w
self.likes = None # Likes rank, appears in the description of w
self.owner = None # Optionally appears in the footer of im and w
self.image = None # URL of the image, appears in im and w
self.creator = None # Needs to be fetched with fetch_extra
self.gender = None # Appears only in the description of im
self.type = None
self.is_claimed = None
patterns = [
"⚠️ {} ROLLS LEFT ⚠️ · Belongs to {}", # Pattern with rolls left
"Belongs to {}", # Simple pattern
]
# Message is missing parts to match against and can't be a match
if message.author != self.mudae or not len(message.embeds) == 1 or message.embeds[0].image is None:
raise WaifuTypeError("Message passed to the Waifu constructor it not a valid mudae message")
embed = message.embeds[0]
desc = embed.description
try:
lines = desc.split("\n")
except:
print("Not True Waifu Message")
return
self.name = embed.author.name
self.image = embed.image.url
first_line = lines[0]
if EMOJI_MALE in first_line:
self.gender = self.Gender.male
first_line = first_line.replace(EMOJI_MALE, "")
elif EMOJI_FEMALE in first_line:
self.gender = self.Gender.female
first_line = first_line.replace(EMOJI_FEMALE, "")
self.series = first_line.strip()
if self.gender is None:
self.type = self.Type.roll
else:
self.type = self.Type.info
if self.image is None:
self.type = self.Type.info
for line in lines:
if EMOJI_KAKERA in line:
self.kakera = parse.search("**{:d}**", line.replace("+", ""))[0]
elif "Claim Rank" in line:
self.claims = parse.search("Claim Rank: #{:d}", line)[0]
elif "Like Rank" in line:
self.likes = parse.search("Like Rank: #{:d}", line)[0]
footer = embed.footer.text
if footer is not None:
# Use a single pattern to extract the owner
match = parse.parse("Belongs to {}", footer)
if match:
self.owner = match[0].strip() # Extract and clean up the owner
self.is_claimed = True # Mark as claimed
else:
self.is_claimed = False # If no match, mark as not claimed
match = parse.search("{:d} / {:d}", footer)
if match is not None and self.gender == self.Type.roll:
raise Exception("This waifu has multiple images but is also missing a gender. this shouldn't happen")
async def fetch_extra(self):
"""
Fills the suitor and creator attributes, by reading messages sent before and after the waifu.
The suitor and creator attributes are by default empty and null respectively. To get the real values, this method must be called.
The method will only work for waifus of type roll and only if the waifu was just rolled.
"""
state = 0
async for message in self.message.channel.history(limit=10):
if state == 0:
if message.id == self.message.id:
state = 1
elif state == 1:
state += 1
if message.author != self.mudae:
self.creator = message.author
break
elif "wished" in message.content.lower():
self.suitors = message.mentions
elif state == 5:
break
else:
state += 1
if message.author != self.mudae:
self.creator = message.author
break
# await asyncio.sleep(1)
# UNTESTED ------------------->
"""
self.message = await self.message.channel.fetch_message(self.message_id)
if self.is_claimed and self.is_roll:
for react in self.message.reactions:
name = react.emoji.name
if "kakera" in name:
name = name.replace("kakera", "")
if name == "":
name = "K"
self.ka_react = name
break
"""
async def await_claim(self):
"""
Waits for a member to claim this waifu, then returns with that member.
If the waifu has already been claimed, the owner is returned immediately.
If the waifu doesn't have an owner, the function will wait for up to 60s for someone to claim.
Returns none if after 60s no one has claimed.
Returns
-------
Waifu
The waifu has an owner or one is found.
None
No owner could be found, the waifu wasn't claimed within 60s.
"""
if self.is_claimed:
return self.owner
def check(message):
return message.author == self.mudae and self.name in message.content and "are now married" in message.content.lower()
try:
message = await self.user.wait_for("message", timeout=60, check=check)
user_name = message.content.split("**")[1]
self.owner = message.guild.get_member_named(user_name)
self.is_claimed = True
return self.owner
except asyncio.TimeoutError:
return None
def __str__(self):
return self.name
class Mudae:
"""
Represents a mudae bot. Primarily used as a factory for Waifu objects.
Before doing anything with this class, make sure you've configured your mudae bot properly.
Kakera value must be visible on rolls, for this class to be able to read the messages from mudae.
If you want to check for claim or roll resets, you must provide the optional timing parameter
----------
mudae: discord.User
The mudea bot.
user: discord.Client
The client that's using this class.
has_timing: bool
If the module was created
Methods
-------
waifu_from(message)
Returns a waifu from a waifu message.
is_wish(message, wishes, check_name, check_series)
Checks if the waifu from a waifu message is part of a list of wishes.
until_roll(in_seconds)
Returns how much time there's left until the next roll reset.
until_claim(in_seconds)
Returns how much time there's left until the next claim reset.
async wait_roll()
Pauses until next roll reset.
async wait_claim()
Pauses until next claim reset.
"""
def __init__(self, user, timing: int=None):
"""
Parameters
----------
user: discord.Client
The client that's using this class.
timing: int
Value encoded with information on claim and roll resets. Use the get_timing method to create one.
"""
self.user = user
self.mudae = user.get_user(MUDA)
if timing:
timings = _split_timing(timing)
self._roll_mod = timings[0]
self._claim_mod = timings[1]
self._roll_rem = timings[2]
self._claim_rem = timings[3]
self.has_timing = True
else:
self.has_timing = False
def waifu_from(self, message):
"""
Returns a waifu from a message.
Currently two types of messages are supported, rolls and infos. Rolls are usually created with the $w command, and infoes with the $im command.
If the message supplied is none of the two valid types of messages, or is not valid for another reason, none is returned.
Parameters
----------
message: discord.Message
A discord message from mudae with a waifu (a waifu message).
Returns
-------
Waifu
A waifu created from the message.
None
The message isn't valid.
"""
try:
return Waifu(self.mudae, self.user, message)
except TypeError:
return None
def from_wish(self, message, wishes: list[str], check_name: bool=True, check_series: bool=False):
"""
Checks if the waifu from a waifu message is part of a list of wishes.
If both check_name and check_series are true, the wishes will be checked against both name and series.
If both are false, the function will always return false.
Parameters
----------
message: discord.Message
A discord waifu message.
wishes: list
A list of strings, where each string is a wish.
check_name: bool
Whether the wishes are wishes for specific waifus.
check_series: bool
Whether the wishes are wishes for specific series.
Returns
-------
bool
Whether the waifu was wished.
"""
waifu = self.waifu_from(message)
wishes = map(lambda wish: wish.lower(), wishes)
if not waifu:
return None
if check_name and waifu.name.lower() in wishes:
return waifu
if check_series and waifu.series.lower() in wishes:
return waifu
return False
def until_roll(self, in_seconds: bool=False):
"""
Returns how much time there's left until the next roll reset.
Parameters
----------
in_seconds: bool
Whether the time returned should be in seconds or minutes.
Returns
-------
int
The time left until next roll reset.
"""
if not self.has_timing:
raise TypeError("Missing cooldown data")
left = self._roll_rem - (int(time.time()) % self._roll_mod)
if left < 0:
left += self._roll_mod
if not in_seconds:
left = int(left / 60)
return left
def until_claim(self, in_seconds: bool=False):
"""
Returns how much time there's left until the next claim reset.
Parameters
----------
in_seconds: bool
Whether the time returned should be in seconds or minutes.
Returns
-------
int
The time left until next claim reset.
"""
if not self.has_timing:
raise TypeError("Missing cooldown data")
left = self._claim_rem - (int(time.time()) % self._claim_mod)
if left < 0:
left += self._claim_mod
if not in_seconds:
left = int(left / 60)
return left
async def wait_roll(self):
"""
Pauses until next roll reset.
"""
await asyncio.sleep(5)
await asyncio.sleep(self.until_roll(True))
async def wait_claim(self):
"""
Pauses until next claim reset.
"""
await asyncio.sleep(5)
await asyncio.sleep(self.until_claim(True))
def get_timing(roll_mod: int, claim_mod: int, roll_rem: int, claim_rem: int, in_seconds=False) -> int:
"""
A static method that returns an integer from the supplied parameters.
The integer may be provided to the Mudae constructor to enable roll and claim cool-down functionality
Parameters
----------
roll_mod: int
The time period, between roll resets. The default mudae value for this is 60 min.
claim_mod: int
The time period, between claim resets. The default mudae value for this is 120 min.
roll_rem: int
The time period, from now until the next roll reset.
claim_rem: int
The time period, from now until the next claim reset.
in_seconds: bool
If the time periods are given as seconds or minutes.
"""
if not in_seconds:
roll_mod *= 60
claim_mod *= 60
roll_rem *= 60
claim_rem *= 60
timings = 0
roll_rem = (int(time.time()) + roll_rem) % roll_mod
claim_rem = (int(time.time()) + claim_rem) % claim_mod
all_vals = (roll_mod, claim_mod, roll_rem, claim_rem)
for value in reversed(all_vals):
timings <<= BIT_SIZE
timings += value
return timings
def _split_timing(timing: int) -> tuple[int,...]:
mask = (1 << BIT_SIZE) - 1
nums = []
for _ in range(4):
nums.append(timing & mask)
timing >>= BIT_SIZE
return tuple(nums)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("--rr", type=int, help="Time until next roll reset", required=True)
parser.add_argument("--cr", type=int, help="Time until next claim reset", required=True)
parser.add_argument("--rm", type=int, help="Time period between each roll reset (Defaults to 60)", default=60)
parser.add_argument("--cm", type=int, help="Time period between each claim reset (Defaults to 180)", default=180)
args = parser.parse_args()
print(get_timing(args.rm, args.cm, args.rr, args.cr))