forked from bbedward/graham_discord_bot
-
Notifications
You must be signed in to change notification settings - Fork 5
/
bot.py
2143 lines (1976 loc) · 79.7 KB
/
bot.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
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import discord
from discord.ext import commands
from discord.ext.commands import Bot
from aiohttp import ClientError
from asyncio import TimeoutError
import random
import secrets
import collections
import random
import re
import errno
import asyncio
import uuid
import datetime
import time
import wallet
import util
import settings
import db
import paginator
logger = util.get_logger("main")
BOT_VERSION = "2.5"
# How many users to display in the top users count
TOP_TIPPERS_COUNT=15
# How many previous giveaway winners to display
WINNERS_COUNT=10
# Minimum Amount for !rain
RAIN_MINIMUM = settings.rain_minimum
# Minimum Amount for !rolesoak
ROLESOAK_MINIMUM = settings.rolesoak_minimum
# Minimum amount for !startgiveaway
GIVEAWAY_MINIMUM = settings.giveaway_minimum
# Giveaway duration
GIVEAWAY_MIN_DURATION = 5
GIVEAWAY_MAX_DURATION = settings.giveaway_max_duration
GIVEAWAY_AUTO_DURATION = settings.giveaway_auto_duration
# Rain Delta (Minutes) - How long to look back for active users for !rain
RAIN_DELTA=30
# Spam Threshold (Seconds) - how long to output certain commands (e.g. bigtippers)
SPAM_THRESHOLD=60
# Send Job (seconds) - process send transactions at this interval
SEND_JOB=10
# Receive check job (seconds) - checks for pending transactions and pockets them
RECEIVE_CHECK_JOB=300
# MAX TX_Retries - If wallet does not indicate a successful send for whatever reason, retry this many times
MAX_TX_RETRIES=3
# Change command prefix to whatever you want to begin commands with
COMMAND_PREFIX=settings.command_prefix
# Withdraw cooldown - how long must wait until wirhdraws
WITHDRAW_COOLDOWN=300
# Pool giveaway auto amount (1%)
TIPGIVEAWAY_AUTO_ENTRY=int(.01 * GIVEAWAY_MINIMUM)
# HELP menu header
AUTHOR_HEADER="BananoBot++ v{0} (BANANO Tip Bot)".format(BOT_VERSION)
# Command DOC (TRIGGER, CMD, Overview, Info)
'''
TRIGGER: Users can get information about this command specifically via help $TRIGGER
CMD: Command overview for help doc
OVERVIEW: General overview of command for overview of command listings
INFO: Detailed command usage/examples/information/etc.
'''
def get_aliases(dict, exclude=''):
'''Returns list of command triggers excluding `exclude`'''
cmds = dict["TRIGGER"]
ret_cmds = []
for cmd in cmds:
if cmd != exclude:
ret_cmds.append(cmd)
return ret_cmds
### All commands
BALANCE = {
"TRIGGER" : ["balance", "bal", "$"],
"CMD" : "{0}balance".format(COMMAND_PREFIX),
"OVERVIEW" : "Display balance of your account",
"INFO" : ("Displays the balance of your tip account (in BANANO) as described:" +
"\nActual Balance: The actual balance in your tip account" +
"\nAvailable Balance: The balance you are able to tip with (Actual - Pending Send)" +
"\nPending Send: Tips you have sent, but have not yet been broadcasted to network" +
"\nPending Receipt: Tips that have been sent to you, but have not yet been pocketed by the node. " +
"\nPending funds will be available for tip/withdraw after they have been pocketed by the node")
}
DEPOSIT ={
"TRIGGER" : ["deposit", "register"],
"CMD" : "{0}deposit or {0}register".format(COMMAND_PREFIX),
"OVERVIEW" : "Shows your account address",
"INFO" : ("Displays your tip bot account address along with a QR code" +
"\n- Send BANANO to this address to increase your tip bot balance" +
"\n- If you do not have a tip bot account yet, this command will create one for you (receiving a tip automatically creates an account too)")
}
WITHDRAW = {
"TRIGGER" : ["withdraw"],
"CMD" : "{0}withdraw, takes: address (optional amount)".format(COMMAND_PREFIX),
"OVERVIEW" : "Allows you to withdraw from your tip account",
"INFO" : ("Withdraws specified amount to specified address, " +
"if amount isn't specified your entire tip account balance will be withdrawn" +
"\nExample: `{0}withdraw ban_133711111111111111111111111111111111111111111111111hifc8npp 1000` - Withdraws 1000 BANANO").format(COMMAND_PREFIX)
}
TIP = {
"TRIGGER" : ["ban", "b"],
"CMD" : "{0}ban, takes: amount <*users>".format(COMMAND_PREFIX),
"OVERVIEW" : "Send a tip to mentioned users",
"INFO" : ("Tip specified amount to mentioned user(s) (minimum tip is 1 BANANO)" +
"\nThe recipient(s) will be notified of your tip via private message" +
"\nSuccessful tips will be deducted from your available balance immediately" +
"\nExample: `{0}ban 2 @user1 @user2` would send 2 to user1 and 2 to user2").format(COMMAND_PREFIX)
}
TIPSPLIT = {
"TRIGGER" : ["bansplit", "bsplit", "bs"],
"CMD" : "{0}bansplit, takes: amount, <*users>".format(COMMAND_PREFIX),
"OVERVIEW" : "Split a tip among mentioned uses",
"INFO" : "Distributes a tip evenly to all mentioned users.\nExample: `{0}bansplit 2 @user1 @user2` would send 1 to user1 and 1 to user2".format(COMMAND_PREFIX)
}
TIPRANDOM = {
"TRIGGER" : ["banrandom", "br"],
"CMD" : "{0}banrandom, takes: amount".format(COMMAND_PREFIX),
"OVERVIEW" : "Tips a random active user",
"INFO" : ("Tips amount to a random active user. Active user list picked using same logic as brain" +
"\n**Minimum banrandom amount: {0} BANANO**").format(settings.tiprandom_minimum)
}
RAIN = {
"TRIGGER" : ["brain"],
"CMD" : "{0}brain, takes: amount".format(COMMAND_PREFIX),
"OVERVIEW" : "Split tip among all active* users",
"INFO" : ("Distribute <amount> evenly to users who are eligible.\n" +
"Eligibility is determined based on your *recent* activity **and** contributions to public channels. " +
"Several factors are considered in picking who receives brain. If you aren't receiving it, you aren't contributing enough or your contributions are low-quality/spammy.\n" +
"Note: Users who have a status of 'offline' or 'do not disturb' do not receive brain.\n" +
"Example: `{0}brain 1000` - distributes 1000 evenly to eligible users (similar to `bansplit`)" +
"\n**Minimum brain amount: {1} BANANO**").format(COMMAND_PREFIX, RAIN_MINIMUM)
}
ROLESOAK = { "TRIGGER" : ["bancitizens", "tc"],
"CMD" : "{0}bancitizens, takes: amount".format(COMMAND_PREFIX),
"OVERVIEW" : "Rain across to all citizens",
"INFO" : ("Distribute amount evenly to users who are citizens and have been active relatively recently.\n" +
"Example: `{0}bancitizens 1000` - distributes 1000 evenly to users in Citizens role (similar to `brain`)" +
"\n**Minimum bancitizens amount: {1} BANANO**").format(COMMAND_PREFIX, ROLESOAK_MINIMUM)
}
START_GIVEAWAY = {
"TRIGGER" : ["giveaway", "sponsorgiveaway"],
"CMD" : "{0}giveaway, takes: amount, fee=(amount), duration=(minutes)".format(COMMAND_PREFIX),
"OVERVIEW" : "Sponsor a giveaway",
"INFO" : ("Start a giveaway with given amount, entry fee, and duration." +
"\nEntry fees are added to the total prize pool" +
"\nGiveaway will end and choose random winner after (duration)" +
"\nExample: `{0}giveaway 1000 fee=5 duration=30` - Starts a giveaway of 1000, with fee of 5, duration of 30 minutes" +
"\n**Minimum required to sponsor a giveaway: {1} BANANO**" +
"\n**Minimum giveaway duration: {2} minutes**" +
"\n**Maximum giveaway duration: {3} minutes**").format(COMMAND_PREFIX, GIVEAWAY_MINIMUM, GIVEAWAY_MIN_DURATION, GIVEAWAY_MAX_DURATION)
}
ENTER = {
"TRIGGER" : ["ticket", "enter", "e"],
"CMD" : "{0}ticket, takes: fee (conditional)".format(COMMAND_PREFIX),
"OVERVIEW" : "Enter the current giveaway",
"INFO" : ("Enter the current giveaway, if there is one. Takes (fee) as argument only if there's an entry fee." +
"\n Fee will go towards the prize pool and be deducted from your available balance immediately" +
"\nExample: `{0}ticket` (to enter a giveaway without a fee), `{0}ticket 10` (to enter a giveaway with a fee of 10)").format(COMMAND_PREFIX)
}
TIPGIVEAWAY = {
"TRIGGER" : ["donate", "tipgiveaway", "d"],
"CMD" : "{0}donate, takes: amount".format(COMMAND_PREFIX),
"OVERVIEW" : "Add to present or future giveaway prize pool",
"INFO" : ("Add <amount> to the current giveaway pool\n"+
"If there is no giveaway, one will be started when minimum is reached." +
"\nTips >= {0} BANANO automatically enter you for giveaways sponsored by the community." +
"\nDonations count towards the next giveaways entry fee" +
"\nExample: `{1}donate 1000` - Adds 1000 to giveaway pool").format(TIPGIVEAWAY_AUTO_ENTRY, COMMAND_PREFIX)
}
TICKETSTATUS = {
"TRIGGER" : ["ticketstatus", "ts"],
"CMD" : "{0}ticketstatus".format(COMMAND_PREFIX),
"OVERVIEW" : "Check if you are entered into the current giveaway",
"INFO" : "Check if you are entered into the current giveaway"
}
GIVEAWAY_STATS= {
"TRIGGER" : ["gstats", "giveawaystats", "gs", "giveawaystatus", "gstatus"],
"CMD" : "{0}giveawaystats or {0}goldenticket".format(COMMAND_PREFIX),
"OVERVIEW" : "Display statistics relevant to the current giveaway",
"INFO" : "Display statistics relevant to the current giveaway"
}
WINNERS = {
"TRIGGER" : ["winners"],
"CMD" : "{0}winners".format(COMMAND_PREFIX),
"INFO" : "Display previous giveaway winners",
"OVERVIEW" : "Display previous giveaway winners"
}
LEADERBOARD = {
"TRIGGER" : ["leaderboard", "ballers", "bigtippers"],
"CMD" : "{0}leaderboard or {0}ballers".format(COMMAND_PREFIX),
"INFO" : "Display the all-time tip leaderboard",
"OVERVIEW" : "Display the all-time tip leaderboard"
}
TOPTIPS = {
"TRIGGER" : ["toptips"],
"CMD" : "{0}toptips".format(COMMAND_PREFIX),
"OVERVIEW" : "Display largest individual tips",
"INFO" : "Display the single largest tips for the past 24 hours, current month, and all time"
}
STATS = {
"TRIGGER" : ["tipstats"],
"CMD" : "{0}tipstats".format(COMMAND_PREFIX),
"OVERVIEW" : "Display your personal tipping stats",
"INFO" : "Display your personal tipping stats (rank, total tipped, and average tip)"
}
ADD_FAVORITE = {
"TRIGGER" : ["addfav", "addfavorite", "addfavourite"],
"CMD" : "{0}addfavorite, takes: *users".format(COMMAND_PREFIX),
"OVERVIEW" : "Add users to your favorites list",
"INFO" : "Adds mentioned users to your favorites list.\nExample: `{0}addfavorite @user1 @user2 @user3` - Adds user1,user2,user3 to your favorites".format(COMMAND_PREFIX)
}
DEL_FAVORITE = {
"TRIGGER" : ["removefavorite", "removefavourite", "removefav"],
"CMD" : "{0}removefavorite, takes: *users or favorite ID".format(COMMAND_PREFIX),
"OVERVIEW" : "Removes users from your favorites list",
"INFO" : ("Removes users from your favorites list. " +
"You can either @mention the user in a public channel or use the ID in your `favorites` list" +
"\nExample 1: `{0}removefavorite @user1 @user2` - Removes user1 and user2 from your favorites" +
"\nExample 2: `{0}removefavorite 1 6 3` - Removes favorites with ID : 1, 6, and 3").format(COMMAND_PREFIX)
}
FAVORITES = {
"TRIGGER" : ["favorites", "favs", "favourites"],
"CMD" : "{0}favorites".format(COMMAND_PREFIX),
"OVERVIEW" : "View your favorites list",
"INFO" : "View your favorites list. Use `{0}addfavorite` to add favorites to your list and `{0}removefavorite` to remove favories".format(COMMAND_PREFIX)
}
TIP_FAVORITES = {
"TRIGGER" : ["tipfavs", "tipfavorites", "tipfavourites", "tf"],
"CMD" : "{0}tipfavorites, takes: amount".format(COMMAND_PREFIX),
"OVERVIEW" : "Tip your entire favorites list",
"INFO" : ("Tip everybody in your favorites list specified amount" +
"\nExample: `{0}tipfavorites 1000` Distributes 1000 to your entire favorites list (similar to `tipsplit`)").format(COMMAND_PREFIX)
}
MUTE = {
"TRIGGER" : ["mute"],
"CMD" : "{0}mute, takes: user id".format(COMMAND_PREFIX),
"OVERVIEW" : "Block tip notifications when sent by this user",
"INFO" : "When someone is spamming you with tips and you can't take it anymore"
}
UNMUTE = {
"TRIGGER" : ["unmute"],
"CMD" : "{0}unmute, takes: user id".format(COMMAND_PREFIX),
"OVERVIEW" : "Unblock tip notificaitons sent by this user",
"INFO" : "When the spam is over and you want to know they still love you"
}
MUTED = {
"TRIGGER" : ["muted"],
"CMD" : "{0}muted".format(COMMAND_PREFIX),
"OVERVIEW" : "View list of users you have muted",
"INFO" : "Are you really gonna drunk dial?"
}
### ADMIN-only commands
FREEZE = {
"CMD" : "{0}freeze, takes: users".format(COMMAND_PREFIX),
"INFO" : "Suspends every action from user, including withdraw"
}
UNFREEZE = {
"CMD" : "{0}unfreeze, takes: users".format(COMMAND_PREFIX),
"INFO" : "Unfreezes mentioned user"
}
FROZEN = {
"CMD" : "{0}frozen".format(COMMAND_PREFIX),
"INFO" : "List frozen users"
}
WALLET_FOR = {
"CMD" : "{0}walletfor, takes: user".format(COMMAND_PREFIX),
"INFO" : "Returns wallet address for mentioned user"
}
USER_FOR_WALLET = {
"CMD" : "{0}userforwallet, takes: user".format(COMMAND_PREFIX),
"INFO" : "Returns user owning wallet address"
}
PAUSE = {
"CMD" : "{0}pause".format(COMMAND_PREFIX),
"INFO" : "Pause all transaction-related activity"
}
UNPAUSE = {
"CMD" : "{0}unpause".format(COMMAND_PREFIX),
"INFO" : "Resume all transaction-related activity"
}
TIPBAN = {
"CMD" : "{0}tipban, takes: users".format(COMMAND_PREFIX),
"INFO" : "Makes it so mentioned users can no longer receive tips"
}
TIPUNBAN = {
"CMD" : "{0}tipunban, takes: users".format(COMMAND_PREFIX),
"INFO" : "Makes it so mentioned users can receive tips again"
}
BANNED = {
"CMD" : "{0}banned".format(COMMAND_PREFIX),
"INFO" : "View list of users currently tip banned"
}
STATSBAN = {
"CMD" : "{0}statsban, takes: users".format(COMMAND_PREFIX),
"INFO" : "Bans mentioned users from all stats consideration"
}
STATSUNBAN = {
"CMD" : "{0}statsunban, takes: users".format(COMMAND_PREFIX),
"INFO" : "Unbans mentioned users from stats considerations"
}
STATSBANNED = {
"CMD" : "{0}statsbanned".format(COMMAND_PREFIX),
"INFO" : "View list of stats banned users"
}
INCREASETIPTOTAL = {
"CMD" : "{0}increasetips (amount) (user)".format(COMMAND_PREFIX),
"INFO" : "Increases users tip total by (amount), for stats purposes"
}
DECREASETIPTOTAL = {
"CMD" : "{0}decreasetips (amount) (user)".format(COMMAND_PREFIX),
"INFO" : "Decreases users tip total by (amount), for stats purposes"
}
SETTOPTIP = {
"CMD" : "{0}settoptip".format(COMMAND_PREFIX),
"INFO" : ("Allows you to set a users top tips. You can set 1 or all of monthly, 24h, and all-time " +
"toptips.\n Example: \n `settoptip @user alltime=2.38 month=1.23 day=0.5` " +
"sets @user's biggest alltime tip to 2.38 BANANO, month to 1.23 BANANO, and day to 0.5 BANANO")
}
INCREASETIPCOUNT = {
"CMD" : "{0}increasetipcount (amount) (user)".format(COMMAND_PREFIX),
"INFO" : "Increases the number of tips a user has made (used for average TIP)"
}
DECREASETIPCOUNT = {
"CMD" : "{0}decreasetipcount (amount) (user)".format(COMMAND_PREFIX),
"INFO" : "Decreases the number of tips a user has made (used for average TIP)"
}
COMMANDS = {
"ACCOUNT_COMMANDS" : [BALANCE, DEPOSIT, WITHDRAW],
"TIPPING_COMMANDS" : [TIP, TIPSPLIT, TIPRANDOM, RAIN, ROLESOAK],
"GIVEAWAY_COMMANDS" : [START_GIVEAWAY, ENTER, TIPGIVEAWAY, TICKETSTATUS],
"STATISTICS_COMMANDS" : [GIVEAWAY_STATS, WINNERS, LEADERBOARD, TOPTIPS,STATS],
"FAVORITES_COMMANDS" : [ADD_FAVORITE, DEL_FAVORITE, FAVORITES, TIP_FAVORITES],
"NOTIFICATION_COMMANDS" : [MUTE, UNMUTE, MUTED],
"ADMIN_COMMANDS" : [FREEZE, UNFREEZE, FROZEN, USER_FOR_WALLET, WALLET_FOR, PAUSE, UNPAUSE, TIPBAN, TIPUNBAN, BANNED, STATSBAN, STATSUNBAN, STATSBANNED, INCREASETIPTOTAL, DECREASETIPTOTAL, SETTOPTIP, INCREASETIPCOUNT, DECREASETIPCOUNT]
}
### Response Templates###
# balance
BALANCE_TEXT=( "```Actual Balance : {0:,.2f} BANANO\n" +
"Available Balance: {1:,.2f} BANANO\n" +
"Pending Send : {2:,.2f} BANANO\n" +
"Pending Receipt : {3:,.2f} BANANO```")
# deposit (split into 3 for easy copypasting address on mobile)
DEPOSIT_TEXT="Your wallet address is:"
DEPOSIT_TEXT_2="{0}"
DEPOSIT_TEXT_3="QR: {0}"
# generic tip replies (apply to numerous tip commands)
INSUFFICIENT_FUNDS_TEXT="You don't have enough BANANO in your available balance!"
TIP_RECEIVED_TEXT="You were tipped {0} BANANO by {1}. You can mute tip notifications from this person using `" + COMMAND_PREFIX + "mute {2}`"
TIP_SELF="No valid recipients found in your tip.\n(You cannot tip yourself and certain other users are exempt from receiving tips)"
# withdraw
WITHDRAW_SUCCESS_TEXT="Withdraw has been queued for processing, I'll send you a link to the transaction after I've broadcasted it to the network!"
WITHDRAW_PROCESSED_TEXT="Withdraw processed:\nTransaction: https://creeper.banano.cc/explorer/block/{0}\nIf you have an issue with a withdraw please wait **24 hours** before contacting my master."
WITHDRAW_NO_BALANCE_TEXT="You have no BANANO to withdraw"
WITHDRAW_INVALID_ADDRESS_TEXT="Withdraw address is not valid"
WITHDRAW_COOLDOWN_TEXT="You need to wait {0} seconds before making another withdraw"
WITHDRAW_INSUFFICIENT_BALANCE="Your balance isn't high enough to withdraw that much"
# leaderboard
TOP_HEADER_TEXT="Here are the top {0} tippers :clap:"
TOP_HEADER_EMPTY_TEXT="The leaderboard is empty!"
TOP_SPAM="No more big tippers for {0} seconds"
# tipstats (individual)
STATS_ACCT_NOT_FOUND_TEXT="I could not find an account for you, try private messaging me `{0}register`".format(COMMAND_PREFIX)
STATS_TEXT="You are rank #{0}, you've tipped a total of {1:.2f} BANANO, your average tip is {2:.2f} BANANO, and your biggest tip of all time is {3:.2f} BANANO"
# tipsplit
TIPSPLIT_SMALL="Tip amount is too small to be distributed to that many users"
# rain
RAIN_NOBODY="I couldn't find anybody eligible to receive rain"
# giveaway (all giveaway related commands)
GIVEAWAY_EXISTS="There's already an active giveaway"
GIVEAWAY_STARTED="{0} has sponsored a giveaway of {1:.2f} BANANO! Use:\n - `" + COMMAND_PREFIX + "ticket` to enter\n - `" + COMMAND_PREFIX + "donate` to increase the pot\n - `" + COMMAND_PREFIX + "ticketstatus` to check the status of your entry"
GIVEAWAY_STARTED_FEE="{0} has sponsored a giveaway of {1:.2f} BANANO! The entry fee is {2} BANANO. Use:\n - `" + COMMAND_PREFIX + "ticket {2}` to buy your ticket\n - `" + COMMAND_PREFIX + "donate` to increase the pot\n - `" + COMMAND_PREFIX + "ticketstatus` to check the status of your entry"
GIVEAWAY_FEE_TOO_HIGH="A giveaway has started where the entry fee is higher than your donations! Use `{0}ticketstatus` to see how much you need to enter!".format(COMMAND_PREFIX)
GIVEAWAY_MAX_FEE="Giveaway entry fee cannot be more than 5% of the prize pool"
GIVEAWAY_ENDED="Congratulations! <@{0}> was the winner of the giveaway! They have been sent {1:.2f} BANANO!"
GIVEAWAY_STATS_NF="There are {0} entries to win {1:.2f} BANANO ending in {2} - sponsored by {3}.\nUse:\n - `" + COMMAND_PREFIX + "ticket` to enter\n - `" + COMMAND_PREFIX + "donate` to add to the pot\n - `" + COMMAND_PREFIX + "ticketstatus` to check status of your entry"
GIVEAWAY_STATS_FEE="There are {0} entries to win {1:.2f} BANANO ending in {2} - sponsored by {3}.\nEntry fee: {4} BANANO. Use:\n - `" + COMMAND_PREFIX + "ticket {4}` to enter\n - `" + COMMAND_PREFIX + "donate` to add to the pot\n - `" + COMMAND_PREFIX + "ticketstatus` to check the status of your entry"
GIVEAWAY_STATS_INACTIVE="There are no active giveaways\n{0} BANANO required to to automatically start one! Use\n - `" + COMMAND_PREFIX + "donate` to donate to the next giveaway.\n - `" + COMMAND_PREFIX + "giveaway` to sponsor your own giveaway\n - `" + COMMAND_PREFIX + "ticketstatus` to see how much you've already donated to the next giveaway"
ENTER_ADDED="You've been successfully entered into the giveaway"
ENTER_DUP="You've already entered the giveaway"
TIPGIVEAWAY_NO_ACTIVE="There are no active giveaways. Check giveaway status using `{0}giveawaystats`, or donate to the next one using `{0}tipgiveaway`".format(COMMAND_PREFIX)
TIPGIVEAWAY_ENTERED_FUTURE="With your bantastic donation I have reserved your ticket for the next community sponsored giveaway!"
# toptips
TOPTIP_SPAM="No more top tips for {0} seconds"
# admin command responses
PAUSE_MSG="All transaction activity is currently suspended. Check back later."
BAN_SUCCESS="User {0} can no longer receive tips"
BAN_DUP="User {0} is already banned"
UNBAN_SUCCESS="User {0} has been unbanned"
UNBAN_DUP="User {0} is not banned"
STATSBAN_SUCCESS="User {0} is no longer considered in tip statistics"
STATSBAN_DUP="User {0} is already stats banned"
STATSUNBAN_SUCCESS="User {0} is now considered in tip statistics"
STATSUNBAN_DUP="User {0} is not stats banned"
# past giveaway winners
WINNERS_HEADER="Here are the previous {0} giveaway winners! :trophy:".format(WINNERS_COUNT)
WINNERS_EMPTY="There are no previous giveaway winners"
WINNERS_SPAM="No more winners for {0} seconds"
# Banano-discord
RIGHTS="```You have been arrested by the BRPD for crimes against the Banano Republic. You have the right to remain unripe. Anything you say can and will be used against you in a banano court. You have the right to have an orangutan. If you cannot afford one, one will be appointed to you by the court. Until your orangutan arrives, you will spend your time in #jail, bail is set at 10 BANANO.```"
RELEASE="```You have been released from Jail!```"
CITIZENSHIP="```I hereby declare you a Citizen of the Banano Republic, may the Banano gods grant you all things which your heart desires.```"
DEPORT="```I hereby withdraw your Citizenship to the Banano Republic, we don’t want to talk to you no more, you empty-headed animal-food-trough wiper. We fart in your general direction. Your mother was a hamster, and your father smelt of elderberries.```"
TROLL="```You have been marked as a TROLL and are no longer a Citizen in the Banano Republic```"
UNTROLL="```You are no longer known as a TROLL in the Banano Republic, please reapply for Citizenship.```"
FROZEN_MSG="Your account is frozen. Contact an admin for help"
### END Response Templates ###
# Paused flag, indicates whether or not bot is paused
paused = False
# Create discord client
client = Bot(command_prefix=COMMAND_PREFIX)
client.remove_command('help')
# Receive check job to pocket transactions
async def receive_check_job():
try:
logger.info("Running receive job...")
accts = []
cursor = db.User.select(db.User.wallet_address)
for a in cursor:
accts.append(a.wallet_address)
accts_pending_action = {
"action":"accounts_pending",
"accounts":accts,
"threshold":100000000000000000000000000000
}
response = await wallet.communicate_wallet_async(accts_pending_action)
if response is None:
response = 'None'
if 'blocks' not in response:
logger.error('invalid response %s. Rescheduling job', str(response))
await schedule_receive_job()
return
for account, blocks in response['blocks'].items():
for b in blocks:
logger.info('Receiving block %s for account %s', b, account)
receive_action = {
"action":"receive",
"wallet":settings.wallet,
"account":account,
"block":b
}
rcv_response = await wallet.communicate_wallet_async(receive_action)
if rcv_response is None:
rcv_response='None'
if 'block' not in rcv_response:
logger.info("Couldn't receive %s - response: %s", b, str(rcv_response))
else:
logger.info("pocketed block %s", b)
logger.info("receive job complete")
await schedule_receive_job()
except (ClientError, TimeoutError):
logger.info("aiohttp error, rescheduling receive_job")
await schedule_receive_job()
except Exception as e:
logger.exception(e)
async def schedule_receive_job():
await asyncio.sleep(RECEIVE_CHECK_JOB)
asyncio.get_event_loop().create_task(receive_check_job())
# TODO
# Would be nice to spawn multiple threads of this at one time.
# Mainly the work_generate process would be nice to multithread
# This isn't as un-optimized as it seems
# Yes a send is a long-running task due to work_generate
# Yes we are not achieving "true" multithreading/multiprocessing with asyncio
# But, the long-running job (work_generate) is executed outside of this program
async def send_job():
try:
logger.info("send_job started")
txs = db.get_unprocessed_transactions()
for tx in txs:
source_address = tx['source_address']
to_address = tx['to_address']
amount = tx['amount']
uid = tx['uid']
attempts = tx['attempts']
raw_withdraw_amt = str(amount) + '00000000000000000000000000000'
wallet_command = {
'action': 'send',
'wallet': settings.wallet,
'source': source_address,
'destination': to_address,
'amount': int(raw_withdraw_amt),
'id': uid
}
src_usr = db.get_user_by_wallet_address(source_address)
trg_usr = db.get_user_by_wallet_address(to_address)
source_id=None
target_id=None
pending_delta = int(amount) * -1
if src_usr is not None:
source_id=src_usr.user_id
if trg_usr is not None:
target_id=trg_usr.user_id
db.mark_transaction_sent(uid, pending_delta, source_id, target_id)
logger.debug("RPC Send")
wallet_output = await wallet.communicate_wallet_async(wallet_command)
logger.debug("RPC Response")
if 'block' in wallet_output:
txid = wallet_output['block']
db.mark_transaction_processed(uid, txid)
logger.info('TX processed. UID: %s, TXID: %s', uid, txid)
if target_id is None:
# Don't wait for the result of this, doesn't matter
asyncio.get_event_loop().create_task(notify_of_withdraw(source_id, txid))
else:
# Not sure what happen but we'll retry a few times
if attempts >= MAX_TX_RETRIES:
logger.info("Max Retires Exceeded for TX UID: %s", uid)
db.mark_transaction_processed(uid, 'invalid')
else:
db.inc_tx_attempts(uid)
logger.info("send_job complete, rescheduling")
await schedule_send_job()
except (ClientError, TimeoutError):
logger.info("aiohttp error, rescheduling send_job")
await schedule_send_job()
except Exception as e:
logger.exception(e)
async def schedule_send_job():
await asyncio.sleep(SEND_JOB)
asyncio.get_event_loop().create_task(send_job())
# Don't make them wait when bot first launches
initial_ts=datetime.datetime.now() - datetime.timedelta(seconds=SPAM_THRESHOLD)
last_big_tippers = {}
last_top_tips = {}
last_winners = {}
last_gs = {}
last_blocks = {}
def create_spam_dicts():
"""map every channel the client can see to datetime objects
this way we can have channel-specific spam prevention"""
global last_big_tippers
global last_top_tips
global last_winners
for c in client.get_all_channels():
if not is_private(c):
last_big_tippers[c.id] = initial_ts
last_top_tips[c.id] = initial_ts
last_winners[c.id] = initial_ts
last_gs[c.id] = initial_ts
last_blocks[c.id] = initial_ts
@client.event
async def on_ready():
logger.info("BananoBot++ v%s started", BOT_VERSION)
logger.info("Discord.py API version %s", discord.__version__)
logger.info("Name: %s", client.user.name)
logger.info("ID: %s", client.user.id)
create_spam_dicts()
await client.change_presence(activity=discord.Game(settings.playing_status))
logger.info("Starting send_job")
asyncio.get_event_loop().create_task(send_job())
logger.info("Continuing outstanding giveaway")
asyncio.get_event_loop().create_task(start_giveaway_timer())
logger.info("Running unsilence job")
asyncio.get_event_loop().create_task(unsilence_users())
logger.info("Starting receive check job")
asyncio.get_event_loop().create_task(receive_check_job())
@client.event
async def on_member_join(member):
if db.silenced(member.id):
muzzled = discord.utils.get(message.guild.roles,name='muzzled')
await member.add_roles(muzzled)
# Periodic check job to unsilence users
async def unsilence_users():
try:
await asyncio.sleep(10)
asyncio.get_event_loop().create_task(unsilence_users())
for s in db.get_silenced():
if s.expiration is None:
continue
elif datetime.datetime.now() >= s.expiration:
for guild in client.guilds:
if guild.id == s.server_id:
muzzled = discord.utils.get(guild.roles,name='muzzled')
for member in guild.members:
if member.id == int(s.user_id):
await member.remove_roles(muzzled)
break
db.unsilence(s.user_id)
except Exception as ex:
logger.exception(ex)
async def notify_of_withdraw(user_id, txid):
"""Notify user of withdraw with a block explorer link"""
user = await client.get_user_info(int(user_id))
await post_dm(user, WITHDRAW_PROCESSED_TEXT, txid)
def is_private(channel):
"""Check if a discord channel is private"""
return isinstance(channel, discord.abc.PrivateChannel)
@client.event
async def on_message(message):
# disregard messages sent by our own bot
if message.author.id == client.user.id:
return
citizen = False
if not is_private(message.channel):
for r in message.author.roles:
if r.name == 'Citizens':
citizen=True
if db.last_msg_check(message.author.id, message.content, is_private(message.channel), citizen) == False:
return
await client.process_commands(message)
def has_admin_role(roles):
"""Check if user has an admin role defined in our settings"""
for r in roles:
if r.name in settings.admin_roles:
return True
return False
async def pause_msg(message):
if paused:
await post_dm(message.author, PAUSE_MSG)
def is_admin(user):
if str(user.id) in settings.admin_ids:
return True
for m in client.get_all_members():
if m.id == user.id:
if has_admin_role(m.roles):
return True
return False
### Commands
def build_page(group_name,commands_dictionary):
entries = []
for cmd in commands_dictionary[group_name]:
entries.append(paginator.Entry(cmd["CMD"],cmd["INFO"]))
return entries
def build_help():
"""Returns an array of paginator.Page objects for help menu"""
pages = []
# Overview
author=AUTHOR_HEADER
title="Command Overview"
description=("Use `{0}help command` for more information about a specific command " +
" or go to the next page").format(COMMAND_PREFIX)
entries = []
tmp_command_list = [
"ACCOUNT_COMMANDS",
"TIPPING_COMMANDS",
"GIVEAWAY_COMMANDS",
"STATISTICS_COMMANDS",
"FAVORITES_COMMANDS",
"NOTIFICATION_COMMANDS"
]
for command_group in tmp_command_list:
for cmd in COMMANDS[command_group]:
entries.append(paginator.Entry(cmd["CMD"],cmd["OVERVIEW"]))
pages.append(paginator.Page(entries=entries, title=title,author=author, description=description))
# Account
author="Account Commands"
description="Check account balance, withdraw, or deposit"
entries = build_page("ACCOUNT_COMMANDS",COMMANDS)
pages.append(paginator.Page(entries=entries, author=author,description=description))
# Tipping
author="Tipping Commands"
description="The different ways you are able to tip with this bot"
entries = build_page("TIPPING_COMMANDS",COMMANDS)
pages.append(paginator.Page(entries=entries, author=author,description=description))
# Giveaway
author="Giveaway Commands"
description="The different ways to interact with the bot's giveaway functionality"
entries = build_page("GIVEAWAY_COMMANDS",COMMANDS)
pages.append(paginator.Page(entries=entries, author=author, description=description))
# Stats
author="Statistics Commands"
description="Individual, bot-wide, and giveaway stats"
entries = build_page("STATISTICS_COMMANDS",COMMANDS)
pages.append(paginator.Page(entries=entries, author=author,description=description))
# Favorites
author="Favorites Commands"
description="How to interact with your favorites list"
entries = build_page("FAVORITES_COMMANDS",COMMANDS)
pages.append(paginator.Page(entries=entries, author=author,description=description))
# notifications
author="Notification Settings"
description="Handle how tip bot gives you notifications"
entries = build_page("NOTIFICATION_COMMANDS",COMMANDS)
pages.append(paginator.Page(entries=entries, author=author, description=description))
# Info
entries = []
author=AUTHOR_HEADER + " - by bbedward"
description=("**Reviews**:\n" + "'10/10 True Masterpiece' - BANANO Core Team" +
"\n'0/10 Didn't get brain' - Almost everybody else\n\n" +
"BANANO Tip Bot is completely free to use and open source." +
" Developed by bbedward (reddit: /u/bbedward, discord: bbedward#9246)" +
"\nFeel free to send tips, suggestions, and feedback.\n\n" +
"github: https://github.com/BananoCoin/Banano-Discord-TipBot/")
pages.append(paginator.Page(entries=entries, author=author,description=description))
return pages
@client.command()
async def help(ctx):
message = ctx.message
# If they spplied an argument post usage for a specific command if applicable
content = message.content.split(' ')
if len(content) > 1:
arg = content[1].strip().lower()
for key, value in COMMANDS.items():
if key == 'ADMIN_COMMANDS':
continue
for v in value:
if arg in v["TRIGGER"]:
await post_usage(message, v)
return
try:
pages = paginator.Paginator(client, message=message, page_list=build_help(),as_dm=True)
await pages.paginate(start_page=1)
except paginator.CannotPaginate as e:
logger.exception(str(e))
@client.command()
async def adminhelp(ctx):
message = ctx.message
if not is_admin(ctx.message.author):
return
embed = discord.Embed(colour=discord.Colour.magenta())
embed.title = "Admin Commands"
for cmd in COMMANDS["ADMIN_COMMANDS"]:
embed.add_field(name=cmd['CMD'], value=cmd['INFO'], inline=False)
await message.author.send(embed=embed)
@client.command(aliases=get_aliases(BALANCE, exclude='balance'))
async def balance(ctx):
message = ctx.message
if is_private(message.channel):
user = db.get_user_by_id(message.author.id, user_name=message.author.name)
if user is None:
return
balances = await wallet.get_balance(user)
actual = balances['actual']
available = balances['available']
send = balances['pending_send']
receive = balances['pending']
await post_response(message, BALANCE_TEXT, actual, available, send, receive)
@client.command(aliases=get_aliases(DEPOSIT, exclude='deposit'))
async def deposit(ctx):
message = ctx.message
if is_private(message.channel):
user = await wallet.create_or_fetch_user(message.author.id, message.author.name)
user_deposit_address = user.wallet_address
await post_response(message, DEPOSIT_TEXT)
await post_response(message, DEPOSIT_TEXT_2, user_deposit_address)
await post_response(message, DEPOSIT_TEXT_3, get_qr_url(user_deposit_address))
@client.command()
async def withdraw(ctx):
message = ctx.message
if paused:
await pause_msg(message)
return
elif db.is_frozen(message.author.id):
await post_dm(message.author, FROZEN_MSG)
elif is_private(message.channel):
try:
withdraw_amount = find_amount(message.content)
except util.TipBotException as e:
withdraw_amount = 0
try:
withdraw_address = find_address(message.content)
user = db.get_user_by_id(message.author.id, user_name=message.author.name)
if user is None:
return
last_withdraw_delta = db.get_last_withdraw_delta(user.user_id)
if WITHDRAW_COOLDOWN > last_withdraw_delta:
raise util.TipBotException("cooldown_error")
source_id = user.user_id
source_address = user.wallet_address
balance = await wallet.get_balance(user)
amount = balance['available']
if withdraw_amount == 0:
withdraw_amount = amount
else:
withdraw_amount = abs(withdraw_amount)
if amount == 0:
await post_response(message, WITHDRAW_NO_BALANCE_TEXT)
elif withdraw_amount > amount:
await post_response(message, WITHDRAW_INSUFFICIENT_BALANCE)
else:
uid = str(uuid.uuid4())
await wallet.make_transaction_to_address(user, withdraw_amount, withdraw_address, uid,verify_address = True)
await post_response(message, WITHDRAW_SUCCESS_TEXT)
db.update_last_withdraw(user.user_id)
except util.TipBotException as e:
if e.error_type == "address_not_found":
await post_usage(message, WITHDRAW)
elif e.error_type == "invalid_address":
await post_response(message, WITHDRAW_INVALID_ADDRESS_TEXT)
elif e.error_type == "balance_error":
await post_response(message, INSUFFICIENT_FUNDS_TEXT)
elif e.error_type == "error":
await post_response(message, WITHDRAW_ERROR_TEXT)
elif e.error_type == "cooldown_error":
await post_response(message, WITHDRAW_COOLDOWN_TEXT, (WITHDRAW_COOLDOWN - last_withdraw_delta))
@client.command(aliases=get_aliases(TIP, exclude='ban'))
async def ban(ctx):
await do_tip(ctx.message)
@client.command(aliases=get_aliases(TIPRANDOM,exclude='banrandom'))
async def banrandom(ctx):
await do_tip(ctx.message, rand=True)
async def do_tip(message, rand=False):
if is_private(message.channel):
return
elif paused:
await pause_msg(message)
return
elif db.is_frozen(message.author.id):
await post_dm(message.author, FROZEN_MSG)
return
try:
user = db.get_user_by_id(message.author.id, user_name=message.author.name)
if user is None:
return
amount = find_amount(message.content)
if rand and amount < settings.tiprandom_minimum:
raise util.TipBotException("usage_error")
# Make sure amount is valid and at least 1 user is mentioned
if amount < 1 or (len(message.mentions) < 1 and not rand):
raise util.TipBotException("usage_error")
# Create tip list
users_to_tip = []
if not rand:
for member in message.mentions:
# Disregard mentions of exempt users and self
if member.id not in settings.exempt_users and member.id != message.author.id and not db.is_banned(member.id) and not member.bot:
users_to_tip.append(member)
if len(users_to_tip) < 1:
raise util.TipBotException("no_valid_recipient")
else:
# Spam Check
spam = db.tiprandom_check(user)
if spam > 0:
await post_dm(message.author, "You need to wait {0} seconds before you can banrandom again", spam)
await add_x_reaction(message)
return
# Pick a random active user
active = db.get_active_users(RAIN_DELTA)
if len(active) == 0:
await post_dm(message.author, "I couldn't find any active user to tip")
return
if str(message.author.id) in active:
active.remove(str(message.author.id))
# Remove bots from consideration
for a in active:
dmember = message.guild.get_member(int(a))
if dmember is None or dmember.bot:
active.remove(a)
sysrand = random.SystemRandom()
sysrand.shuffle(active)
offset = secrets.randbelow(len(active))
users_to_tip.append(message.guild.get_member(int(active[offset])))
# Cut out duplicate mentions
users_to_tip = list(set(users_to_tip))
# Make sure this user has enough in their balance to complete this tip
required_amt = amount * len(users_to_tip)
balance = await wallet.get_balance(user)
user_balance = balance['available']
if user_balance < required_amt:
await add_x_reaction(message)
await post_dm(message.author, INSUFFICIENT_FUNDS_TEXT)
return
# Distribute tips
for member in users_to_tip:
uid = str(uuid.uuid4())
actual_amt = await wallet.make_transaction_to_user(user, amount, member.id, member.name, uid)
# Something went wrong, tip didn't go through
if actual_amt == 0:
required_amt -= amount
else:
msg = TIP_RECEIVED_TEXT
if rand:
msg += ". You were randomly chosen by {0}'s `banrandom`".format(message.author.name)
await post_dm(message.author, "{0} was the recipient of your random {1} BANANO tip", member.name, actual_amt, skip_dnd=True)
if not db.muted(member.id, message.author.id):
await post_dm(member, msg, actual_amt, message.author.name, message.author.id, skip_dnd=True)
# Post message reactions
await react_to_message(message, required_amt)
# Update tip stats
if message.channel.id not in (416306340848336896, 443985110371401748) and not user.stats_ban:
db.update_tip_stats(user, required_amt)
except util.TipBotException as e:
if e.error_type == "amount_not_found" or e.error_type == "usage_error":
if rand:
await post_usage(message, TIPRANDOM)
else:
await post_usage(message, TIP)
elif e.error_type == "no_valid_recipient":
await post_dm(message.author, TIP_SELF)
@client.command(aliases=get_aliases(TIPSPLIT, exclude='bansplit'))
async def bansplit(ctx):
await do_tipsplit(ctx.message)
async def do_tipsplit(message, user_list=None):
if is_private(message.channel):
return
elif paused:
await pause_msg(message)
return
elif db.is_frozen(message.author.id):
await post_dm(message.author, FROZEN_MSG)
return
try:
amount = find_amount(message.content)
# Make sure amount is valid and at least 1 user is mentioned
if amount < 1 or (len(message.mentions) < 1 and user_list is None):
raise util.TipBotException("usage_error")
# Create tip list
users_to_tip = []
if user_list is not None:
for m in message.mentions:
user_list.append(m)
else:
user_list = message.mentions
if int(amount / len(user_list)) < 1: