1
|
|
|
import re |
2
|
|
|
import textwrap |
3
|
|
|
import os |
4
|
|
|
import sys |
5
|
|
|
from datetime import datetime |
6
|
|
|
from PIL import Image, ImageFont, ImageDraw, ImageOps |
7
|
|
|
from pincer import command, Client |
8
|
|
|
from pincer.commands import CommandArg, Description |
9
|
|
|
from pincer.objects import Message, Embed, MessageContext |
10
|
|
|
|
11
|
|
|
|
12
|
|
|
# you need to manually download the font files and put them into the folder |
13
|
|
|
# ./examples/tweet_generator/ to make the script works using this link: |
14
|
|
|
# https://fonts.google.com/share?selection.family=Noto%20Sans:wght@400;700 |
15
|
|
|
if not all(font in os.listdir() for font in ["NotoSans-Regular.ttf", "NotoSans-Bold.ttf"]): |
16
|
|
|
sys.exit() |
17
|
|
|
|
18
|
|
|
|
19
|
|
|
class Bot(Client): |
20
|
|
|
@Client.event |
21
|
|
|
async def on_ready(self): |
22
|
|
|
print( |
23
|
|
|
f"Started client on {self.bot}\n" |
24
|
|
|
f"Registered commands: {', '.join(self.chat_commands)}" |
25
|
|
|
) |
26
|
|
|
|
27
|
|
|
@command(description="to create fake tweets") |
28
|
|
|
async def twitter( |
29
|
|
|
self, |
30
|
|
|
ctx: MessageContext, |
31
|
|
|
content: CommandArg[str, Description["The content of the message"]] |
32
|
|
|
): |
33
|
|
|
await ctx.interaction.ack() |
34
|
|
|
|
35
|
|
|
for text_match, user_id in re.findall( |
36
|
|
|
re.compile(r"(<@!(\d+)>)"), content |
37
|
|
|
): |
38
|
|
|
content = content.replace( |
39
|
|
|
text_match, f"@{await self.get_user(user_id)}" |
40
|
|
|
) |
41
|
|
|
|
42
|
|
|
if len(content) > 280: |
43
|
|
|
return "A tweet can be at maximum 280 characters long" |
44
|
|
|
|
45
|
|
|
# download the profile picture and convert it into Image object |
46
|
|
|
avatar = (await ctx.author.user.get_avatar()).resize((128, 128)) |
47
|
|
|
avatar = circular_avatar(avatar) |
48
|
|
|
|
49
|
|
|
# create the tweet by pasting the profile picture into a white image |
50
|
|
|
tweet = trans_paste( |
51
|
|
|
avatar, |
52
|
|
|
Image.new("RGBA", (800, 250 + 50 * len(textwrap.wrap(content, 38))), (255, 255, 255)), |
53
|
|
|
box=(15, 15), |
54
|
|
|
) |
55
|
|
|
|
56
|
|
|
# add the fonts |
57
|
|
|
font_normal = ImageFont.truetype("NotoSans-Regular.ttf", 40) |
58
|
|
|
font_small = ImageFont.truetype("NotoSans-Regular.ttf", 30) |
59
|
|
|
font_bold = ImageFont.truetype("NotoSans-Bold.ttf", 40) |
60
|
|
|
|
61
|
|
|
# write the name and username on the Image |
62
|
|
|
draw = ImageDraw.Draw(tweet) |
63
|
|
|
draw.text( |
64
|
|
|
(180, 20), str(ctx.author.user), |
65
|
|
|
fill=(0, 0, 0), font=font_bold |
66
|
|
|
) |
67
|
|
|
draw.text( |
68
|
|
|
(180, 70), |
69
|
|
|
f"@{ctx.author.user.username}", |
70
|
|
|
fill=(120, 120, 120), font=font_normal, |
71
|
|
|
) |
72
|
|
|
|
73
|
|
|
content = add_color_to_mentions(content) |
74
|
|
|
|
75
|
|
|
# write the text |
76
|
|
|
tweet = draw_multicolored_text(tweet, content, font_normal) |
77
|
|
|
|
78
|
|
|
# write the footer |
79
|
|
|
draw.text( |
80
|
|
|
(30, tweet.size[1] - 60), |
81
|
|
|
datetime.now().strftime( |
82
|
|
|
"%I:%M %p · %d %b. %Y · Twitter for Discord" |
83
|
|
|
), |
84
|
|
|
fill=(120, 120, 120), |
85
|
|
|
font=font_small, |
86
|
|
|
) |
87
|
|
|
|
88
|
|
|
return Message( |
89
|
|
|
embeds=[ |
90
|
|
|
Embed(title="Twitter for Discord").set_image( |
91
|
|
|
url="attachment://image0.png" |
92
|
|
|
) |
93
|
|
|
], |
94
|
|
|
attachments=[tweet], |
95
|
|
|
) |
96
|
|
|
|
97
|
|
|
|
98
|
|
|
def trans_paste(fg_img, bg_img, box=(0, 0)): |
99
|
|
|
""" |
100
|
|
|
https://stackoverflow.com/a/53663233/15485584 |
101
|
|
|
paste an image into one another |
102
|
|
|
""" |
103
|
|
|
fg_img_trans = Image.new("RGBA", fg_img.size) |
104
|
|
|
fg_img_trans = Image.blend(fg_img_trans, fg_img, 1.0) |
105
|
|
|
bg_img.paste(fg_img_trans, box, fg_img_trans) |
106
|
|
|
return bg_img |
107
|
|
|
|
108
|
|
|
|
109
|
|
|
def circular_avatar(avatar): |
110
|
|
|
mask = Image.new("L", (128, 128), 0) |
111
|
|
|
draw = ImageDraw.Draw(mask) |
112
|
|
|
draw.ellipse((0, 0, 128, 128), fill=255) |
113
|
|
|
avatar = ImageOps.fit(avatar, mask.size, centering=(0.5, 0.5)) |
114
|
|
|
avatar.putalpha(mask) |
115
|
|
|
return avatar |
116
|
|
|
|
117
|
|
|
|
118
|
|
|
def add_color_to_mentions(message): |
119
|
|
|
""" |
120
|
|
|
generate a dict to set were the text need to be in different colors. |
121
|
|
|
if a word starts with '@' it will be write in blue. |
122
|
|
|
|
123
|
|
|
Parameters |
124
|
|
|
---------- |
125
|
|
|
message: the text |
126
|
|
|
|
127
|
|
|
Returns |
128
|
|
|
------- |
129
|
|
|
a list with all colors selected |
130
|
|
|
example: |
131
|
|
|
[ |
132
|
|
|
{'color': (0, 0, 0), 'text': 'hello world '}, |
133
|
|
|
{'color': (0, 154, 234), 'text': '@drawbu'} |
134
|
|
|
] |
135
|
|
|
|
136
|
|
|
""" |
137
|
|
|
message = textwrap.wrap(message, 38) |
138
|
|
|
message = "\n".join(message).split(" ") |
139
|
|
|
result = [] |
140
|
|
|
for word in message: |
141
|
|
|
wordlines = word.splitlines() |
142
|
|
|
for index, text in enumerate(wordlines): |
143
|
|
|
|
144
|
|
|
text += "\n" if index != len(wordlines) - 1 else " " |
145
|
|
|
|
146
|
|
|
if not result: |
147
|
|
|
result.append({"color": (0, 0, 0), "text": text}) |
148
|
|
|
continue |
149
|
|
|
|
150
|
|
|
if not text.startswith("@"): |
151
|
|
|
if result[-1]["color"] == (0, 0, 0): |
152
|
|
|
result[-1]["text"] += text |
153
|
|
|
continue |
154
|
|
|
|
155
|
|
|
result.append({"color": (0, 0, 0), "text": text}) |
156
|
|
|
continue |
157
|
|
|
|
158
|
|
|
result.append({"color": (0, 154, 234), "text": text}) |
159
|
|
|
return result |
160
|
|
|
|
161
|
|
|
|
162
|
|
|
def draw_multicolored_text(image, message, font): |
163
|
|
|
draw = ImageDraw.Draw(image) |
164
|
|
|
x = 30 |
165
|
|
|
y = 170 |
166
|
|
|
y_fontsize = font.getsize(" ")[1] |
167
|
|
|
for text in message: |
168
|
|
|
y -= y_fontsize |
169
|
|
|
for l_index, line in enumerate(text["text"].splitlines()): |
170
|
|
|
if l_index: |
171
|
|
|
x = 30 |
172
|
|
|
y += y_fontsize |
173
|
|
|
draw.text((x, y), line, fill=text["color"], font=font) |
174
|
|
|
x += font.getsize(line)[0] |
175
|
|
|
return image |
176
|
|
|
|
177
|
|
|
|
178
|
|
|
if __name__ == "__main__": |
179
|
|
|
# Of course we have to run our client, you can replace the |
180
|
|
|
# XXXYOURBOTTOKENHEREXXX with your token, or dynamically get it |
181
|
|
|
# through a dotenv/env. |
182
|
|
|
Bot("XXXYOURBOTTOKENHEREXXX").run() |
183
|
|
|
|