├── .github
└── dependabot.yml
├── .gitignore
├── LICENSE
├── README.md
├── dispander
├── __init__.py
└── module.py
├── samplebot.py
└── setup.py
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: pip
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "09:00"
8 | timezone: Asia/Tokyo
9 | open-pull-requests-limit: 99
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 1ntegrale9
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # dispander (Discord Message URL Expander)
2 | DiscordのメッセージURLを検知して展開する機能を追加する discord.py Bot拡張用ライブラリ
3 |
4 |
5 |
6 | # 使い方
7 |
8 | `python3 -m pip install dispander`
9 |
10 | ## extensionとして使用する場合
11 |
12 | load_extensionで読み込んでください
13 |
14 | ```python
15 | from discord.ext import commands
16 |
17 | bot = commands.Bot(command_prefix='/')
18 | bot.load_extension('dispander')
19 | bot.run(token)
20 | ```
21 |
22 | ## 関数として使用する場合
23 |
24 | on_message内のどこかで実行してください。
25 |
26 | 展開したメッセージを消去する機能を使用するには`on_reaction_add`イベントもしくは`on_raw_reaction_add`イベントのどちらかでdelete_dispand関数を実行してください。
27 | on_raw_reaction_addの場合はキーワード引数`payload`にRawReactionActionEventを、on_reaction_addの場合はキーワード引数`user`にUser、`reaction`にReactionを指定して下さい。
28 |
29 | 消去の際のリアクションを変更したい場合は環境変数`DELETE_REACTION_EMOJI`に絵文字を設定してください。
30 |
31 | ```python
32 | import discord
33 | from dispander import dispand, delete_dispand
34 |
35 | client = discord.Client()
36 |
37 | @client.event
38 | async def on_message(message):
39 | if message.author.bot:
40 | return
41 | await dispand(message)
42 |
43 |
44 | @client.event
45 | async def on_raw_reaction_add(payload):
46 | await delete_dispand(client, payload=payload)
47 |
48 |
49 | client.run(token)
50 | ```
51 |
--------------------------------------------------------------------------------
/dispander/__init__.py:
--------------------------------------------------------------------------------
1 | from dispander.module import *
2 |
3 |
--------------------------------------------------------------------------------
/dispander/module.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | import os
3 |
4 | import discord
5 | from discord import Embed
6 | from discord.ext import commands
7 | import re
8 |
9 | regex_discord_message_url = (
10 | '(?!<)https://(ptb.|canary.)?discord(app)?.com/channels/'
11 | '(?P[0-9]{17,20})/(?P[0-9]{17,20})/(?P[0-9]{17,20})(?!>)'
12 | )
13 | regex_extra_url = (
14 | r'\?base_aid=(?P[0-9]{17,20})'
15 | '&aid=(?P[0-9]{17,20})'
16 | '&extra=(?P(|[0-9,]+))'
17 | )
18 | DELETE_REACTION_EMOJI = os.environ.get("DELETE_REACTION_EMOJI", "\U0001f5d1")
19 |
20 |
21 | class ExpandDiscordMessageUrl(commands.Cog):
22 | def __init__(self, bot):
23 | self.bot = bot
24 |
25 | @commands.Cog.listener()
26 | async def on_message(self, message):
27 | if message.author.bot:
28 | return
29 | await dispand(message)
30 |
31 | @commands.Cog.listener()
32 | async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
33 | await delete_dispand(self.bot, payload=payload)
34 |
35 |
36 | async def delete_dispand(bot: discord.Client,
37 | *,
38 | payload: Optional[discord.RawReactionActionEvent] = None,
39 | reaction: Optional[discord.Reaction] = None,
40 | user: Optional[discord.User] = None):
41 | if payload is not None:
42 | # when on_raw_reaction_add event
43 | if str(payload.emoji) != DELETE_REACTION_EMOJI:
44 | return
45 | if payload.user_id == bot.user.id:
46 | return
47 |
48 | channel = bot.get_channel(payload.channel_id)
49 | message = await channel.fetch_message(payload.message_id)
50 | await _delete_dispand(bot, message, payload.user_id)
51 | elif reaction is not None:
52 | # when on_reaction_add event
53 | if str(reaction.emoji) != DELETE_REACTION_EMOJI:
54 | return
55 | if user.id == bot.user.id:
56 | return
57 | await _delete_dispand(bot, reaction.message, user.id)
58 | else:
59 | raise ValueError("payload or reaction must be setted")
60 |
61 |
62 | async def _delete_dispand(bot: discord.Client, message: discord.Message, operator_id: int):
63 | if message.author.id != bot.user.id:
64 | return
65 | elif not message.embeds:
66 | return
67 |
68 | embed = message.embeds[0]
69 | if getattr(embed.author, "url", None) is None:
70 | return
71 | data = from_jump_url(embed.author.url)
72 | if not (data["base_author_id"] == operator_id or data["author_id"] == operator_id):
73 | return
74 | await message.delete()
75 | for message_id in data["extra_messages"]:
76 | extra_message = await message.channel.fetch_message(message_id)
77 | if extra_message is not None:
78 | await extra_message.delete()
79 |
80 |
81 | async def dispand(message):
82 | messages = await extract_message(message)
83 | for m in messages:
84 | sent_messages = []
85 |
86 | if m.content or m.attachments:
87 | sent_message = await message.channel.send(embed=compose_embed(m))
88 | sent_messages.append(sent_message)
89 | # Send the second and subsequent attachments with embed (named 'embed') respectively:
90 | for attachment in m.attachments[1:]:
91 | embed = Embed()
92 | embed.set_image(
93 | url=attachment.proxy_url
94 | )
95 | sent_attachment_message = await message.channel.send(embed=embed)
96 | sent_messages.append(sent_attachment_message)
97 |
98 | for embed in m.embeds:
99 | sent_embed_message = await message.channel.send(embed=embed)
100 | sent_messages.append(sent_embed_message)
101 |
102 | # 一番先頭のメッセージにゴミ箱のリアクションをつける
103 | main_message = sent_messages.pop(0)
104 | await main_message.add_reaction(DELETE_REACTION_EMOJI)
105 | main_embed = main_message.embeds[0]
106 | main_embed.set_author(
107 | name=getattr(main_embed.author, "name", None),
108 | icon_url=getattr(main_embed.author, "icon_url", None),
109 | url=make_jump_url(message, m, sent_messages)
110 | )
111 | await main_message.edit(embed=main_embed)
112 |
113 |
114 | async def extract_message(message):
115 | messages = []
116 | for ids in re.finditer(regex_discord_message_url, message.content):
117 | if message.guild.id != int(ids['guild']):
118 | continue
119 | fetched_message = await fetch_message_from_id(
120 | guild=message.guild,
121 | channel_id=int(ids['channel']),
122 | message_id=int(ids['message']),
123 | )
124 | messages.append(fetched_message)
125 | return messages
126 |
127 |
128 | async def fetch_message_from_id(guild, channel_id, message_id):
129 | channel = guild.get_channel(channel_id)
130 | message = await channel.fetch_message(message_id)
131 | return message
132 |
133 |
134 | def make_jump_url(base_message, dispand_message, extra_messages):
135 | """
136 | make jump url which include more information
137 | :param base_message: メッセージリンクが貼られていたメッセージ
138 | :param dispand_message: 展開中のメッセージ
139 | :param extra_messages: 展開する際にでた二つ目以降のメッセージ(e.g. 画像やembed)
140 | :return: 混入が完了したメッセージリンク
141 | """
142 | # base_aid: メッセージリンクで飛べる最初のメッセージの送信者のid
143 | # aid: メッセージリンクを送信したユーザーのid
144 | return "{0.jump_url}?base_aid={1.id}&aid={2.id}&extra={3}".format(
145 | dispand_message,
146 | dispand_message.author,
147 | base_message.author,
148 | ",".join([str(i.id) for i in extra_messages])
149 | )
150 |
151 |
152 | def from_jump_url(url):
153 | """
154 | メッセージリンクから情報を取得します。
155 | :param url: メッセージリンク
156 | :return: dict
157 | """
158 | base_url_match = re.match(regex_discord_message_url + regex_extra_url, url)
159 | data = base_url_match.groupdict()
160 | return {
161 | "base_author_id": int(data["base_author_id"]),
162 | "author_id": int(data["author_id"]),
163 | "extra_messages": [int(_id) for _id in data["extra_messages"].split(",")] if data["extra_messages"] else []
164 | }
165 |
166 |
167 | def compose_embed(message):
168 | embed = Embed(
169 | description=message.content,
170 | timestamp=message.created_at,
171 | )
172 | embed.set_author(
173 | name=message.author.display_name,
174 | icon_url=message.author.avatar or f'{discord.Asset.BASE}/embed/avatars/{discord.DefaultAvatar.red}.png',
175 | url=message.jump_url
176 | )
177 | embed.set_footer(
178 | text=message.channel.name,
179 | icon_url=message.guild.icon or f'{discord.Asset.BASE}/embed/avatars/{discord.DefaultAvatar.red}.png',
180 | )
181 | if message.attachments and message.attachments[0].proxy_url:
182 | embed.set_image(
183 | url=message.attachments[0].proxy_url
184 | )
185 | return embed
186 |
187 |
188 | def setup(bot):
189 | bot.add_cog(ExpandDiscordMessageUrl(bot))
190 |
--------------------------------------------------------------------------------
/samplebot.py:
--------------------------------------------------------------------------------
1 | from discord.ext import commands
2 | import os
3 |
4 | if __name__ == '__main__':
5 | bot = commands.Bot(command_prefix='/')
6 | bot.load_extension('dispander')
7 | bot.run(os.getenv('DISCORD_BOT_TOKEN'))
8 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="dispander",
8 | version="0.5.0",
9 | author="1ntegrale9",
10 | author_email="1ntegrale9uation@gmail.com",
11 | description="Discord Message URL Expander",
12 | long_description=long_description,
13 | long_description_content_type="text/markdown",
14 | url="https://github.com/DiscordBotPortalJP/dispander",
15 | packages=setuptools.find_packages(),
16 | classifiers=[
17 | "Programming Language :: Python :: 3.7",
18 | "License :: OSI Approved :: MIT License",
19 | "Operating System :: OS Independent",
20 | ],
21 | install_requires=[
22 | "discord.py >= 1.3.3",
23 | ],
24 | )
25 |
--------------------------------------------------------------------------------