diff --git a/__pycache__/NarratorAi.cpython-312.pyc b/__pycache__/NarratorAi.cpython-312.pyc index 053886a..561bde8 100644 Binary files a/__pycache__/NarratorAi.cpython-312.pyc and b/__pycache__/NarratorAi.cpython-312.pyc differ diff --git a/__pycache__/NarratorAi.cpython-313.pyc b/__pycache__/NarratorAi.cpython-313.pyc index 311cbcd..cedd536 100644 Binary files a/__pycache__/NarratorAi.cpython-313.pyc and b/__pycache__/NarratorAi.cpython-313.pyc differ diff --git a/__pycache__/bridges.cpython-312.pyc b/__pycache__/bridges.cpython-312.pyc new file mode 100644 index 0000000..78ff3b8 Binary files /dev/null and b/__pycache__/bridges.cpython-312.pyc differ diff --git a/__pycache__/discord.cpython-312.pyc b/__pycache__/discord.cpython-312.pyc new file mode 100644 index 0000000..fe8b7ef Binary files /dev/null and b/__pycache__/discord.cpython-312.pyc differ diff --git a/__pycache__/mattermost_bridge.cpython-312.pyc b/__pycache__/mattermost_bridge.cpython-312.pyc new file mode 100644 index 0000000..e663fae Binary files /dev/null and b/__pycache__/mattermost_bridge.cpython-312.pyc differ diff --git a/__pycache__/mattermost_bridge.cpython-313.pyc b/__pycache__/mattermost_bridge.cpython-313.pyc new file mode 100644 index 0000000..66f1715 Binary files /dev/null and b/__pycache__/mattermost_bridge.cpython-313.pyc differ diff --git a/__pycache__/misskey.cpython-312.pyc b/__pycache__/misskey.cpython-312.pyc new file mode 100644 index 0000000..a8a36eb Binary files /dev/null and b/__pycache__/misskey.cpython-312.pyc differ diff --git a/bridges.py b/bridges.py new file mode 100644 index 0000000..de912e0 --- /dev/null +++ b/bridges.py @@ -0,0 +1,189 @@ +import requests, re +from mattermostdriver import Driver +from bs4 import BeautifulSoup +from io import BytesIO + +mattermostDriver = Driver({ + "url": "192.168.1.67", + "port": 8065, + "scheme": "http", + "token": "dmefeb8t7pditf3ot9377hrxch" +}) + +mattermostDriver.login() + + +def parse_links_from_mattermost(string): + linkmatches = re.compile(r'\((https?://[^\s)]+)\)') + fullmatches = re.compile(r'!\[.*?\]\(.*?\)') + + links = linkmatches.findall(string) + full = fullmatches.findall(string) + + for match in full: + string = string.replace(match, "") + + for match in links: + string += match + + return string + + +DISCORD_BOT_URL = 'http://localhost:5001/post_message' + +def send_mattermost_to_discord(post, channel_to, avatar_url=None): + + post_message = parse_links_from_mattermost(post['message']) + + username = "Unknown" + if post['user']['nickname'] == "": + username = post['user']['username'] + else: + username = post['user']['nickname'] + + if avatar_url: + payload = { + "message": post_message, + "channel_to": channel_to, + "username": username, + "avatar_url": avatar_url, + "discord_id": post['discord_id'] + } + else: + payload = { + "message": post_message, + "channel_to": channel_to, + "username": username, + "avatar_url": None, + "discord_id": post['discord_id'] + } + + response = requests.post(DISCORD_BOT_URL, json=payload) + + if response.status_code == 200: + print("Message sent successfully.") + else: + print("Failed to send the message.") + print(response.text) + +def parse_links_from_discord(string): + linkmatches = re.compile(r'\bhttps?://(?:tenor|giphy)\.com/\S+\b') + links = linkmatches.findall(string) + + for match in links: + response = requests.get(match) + soup = BeautifulSoup(response.text, 'html.parser') + gif_title = soup.find('meta', property='og:title')['content'] + gif_url = soup.find('meta', property='og:image')['content'] + + string = string.replace(match, "") + string += f"![{gif_title}]({gif_url})" + + return string + +def send_discord_to_mattermost(message, channel_to): + bearer_token = "g9rpuzcpkpgyddozad6j5nui9r" + url = "http://chat.treehousefullofstars.com/api/v4/posts" + webhook_url = "https://chat.treehousefullofstars.com/hooks/qjxs466x1pr63y71bszze56x4c" + + channel_id = mattermostDriver.channels.get_channel_by_name_and_team_name('Treehousefullofstars', channel_to)['id'] + + new_contents = "" + count = 0 + file_ids = [] + """if message.embeds: + print(message.embeds) + for embed in message.embeds: + new_contents += f"\n{embed.url}" + count += 1 + + file_url = embed.url + + response = requests.get(file_url) + file_data = response.content + + file_id = mattermostDriver.files.upload_file( + channel_id=channel_id, + files={'files': (embed.filename, file_data)} + )['file_infos'][0]['id'] + file_ids.append(file_id)""" + + if message.attachments: + print(message.attachments) + for attachment in message.attachments: + file_url = attachment.url + + response = requests.get(file_url) + file_data = response.content + + file_id = mattermostDriver.files.upload_file( + channel_id=channel_id, + files={'files': (attachment.filename, file_data)} + )['file_infos'][0]['id'] + file_ids.append(file_id) + + + """payload = { + "channel": channel_to, + "text": message.content, + "username": message.author.nick, + "icon_url": message.author.display_avatar.url, + "file_ids": file_ids + } + response = requests.post(webhook_url, json=payload) + print(response.content)""" + + text = message.content + if count>0: + text += new_contents + + text = parse_links_from_discord(text) + + response = mattermostDriver.posts.create_post(options={ + 'channel_id': channel_id, + 'props':{ + 'from_webhook': 'true', + 'override_username': message.author.nick, + 'override_icon_url': message.author.display_avatar.url + }, + 'message': text, + 'file_ids': file_ids}) + +def send_misskey_to_mattermost(event, channel_id): + if event['body']['body']['renoteId']: + files = event['body']['body']['renote'].get('files', None) + else: + files = event['body']['body'].get('files', None) + + file_ids=[] + if files != []: + for i, file in enumerate(files): + response = requests.get(file['url']) + file_data = BytesIO(response.content) + filename = file['name'] + file_id = mattermostDriver.files.upload_file( + channel_id=channel_id, + files={'files': (filename, file_data)} + )['file_infos'][0]['id'] + file_ids.append(file_id) + + if event['body']['body']['renoteId']: + renote_user = event['body']['body']['renote']['user'].get('name', 'unknown') + renote_username = event['body']['body']['renote']['user'].get('username', 'unknown') + renote_user_host = event['body']['body']['renote']['user'].get('hose', 'unknown') + note_content = f"*Renoted {renote_user} (@{renote_username}@{renote_user_host})*\n{event['body']['body']['renote']['text']}" + else: + note_content = event['body']['body'].get('text', 'No text in note') + + username = event['body']['body']['user'].get('name', 'Misskey Bot') + avatar_url = event['body']['body']['user'].get('avatarUrl', '') + + response = mattermostDriver.posts.create_post(options={ + 'channel_id': channel_id, + 'props':{ + 'from_webhook': 'true', + 'override_username':username, + 'override_icon_url': avatar_url + }, + 'message': note_content, + 'file_ids': file_ids}) \ No newline at end of file diff --git a/bot.py b/discordapp.py similarity index 52% rename from bot.py rename to discordapp.py index 9164140..1389df0 100644 --- a/bot.py +++ b/discordapp.py @@ -1,8 +1,11 @@ import discord -from discord import app_commands +#from discord import app_commands from NarratorAi import setScene, setItem, setRollDice import random - +import flask +import bridges +from threading import Thread +import requests guild_id = 954201387770736751 @@ -10,7 +13,9 @@ intents = discord.Intents.default() intents.message_content = True client = discord.Client(intents=intents) -tree = app_commands.CommandTree(client) +tree = discord.app_commands.CommandTree(client) + + @tree.command(name="roll_die", description="Ask NARC to roll dice for you with flair.", @@ -63,13 +68,73 @@ async def on_ready(): await tree.sync(guild=discord.Object(id=guild_id)) print(f'We have logged in as {client.user}') + +channels = { + 1125968295967850559: "our-comforter", #comforter + 954201387770736754: "lounge", #lounge + 1119502004721557554: "town-square", # kweh + 1367978276185964584: "misskey", #misskey + 1167176429797113926: "photos-from-another-star", #photos-from-another-star + 1119508652844404816: "bulletin-board", #bulletin-board + 955394194766192690: "photos-of-the-gang" #photos-of-the-gang +} + @client.event async def on_message(message): if message.author == client.user: return - if message.content.startswith('$hello'): - await message.channel.send('Hello!') + if not message.webhook_id and message.channel.id in channels.keys(): + bridges.send_discord_to_mattermost(message, channel_to=channels[message.channel.id]) + + +async def send_custom_message(channel: discord.TextChannel, message, username, user_id, avatar_url=None): + if user_id: + print(user_id) + member = await channel.guild.fetch_member(user_id) + avatar_url = member.display_avatar.url + username = member.nick + elif avatar_url: + avatar_url = avatar_url + else: + avatar_url = "" + + print(avatar_url) + webhook = await channel.create_webhook(name=username) + await webhook.send(message, username=username, avatar_url=avatar_url) + await webhook.delete() + +# flask +app = flask.Flask(__name__) + +@app.route('/post_message', methods=['POST']) +async def post_message(): + data:dict = flask.request.json + message = data.get('message') + username = data.get('username', 'Unknown') + avatar_url = data.get('avatar_url', None) + user_id = data.get('discord_id', None) + print(data) + if message == "": + return flask.jsonify({'error': 'Message cannot be blank.'}), 400 + + channel = client.get_channel(data['channel_to']) + if channel: + client.loop.create_task(send_custom_message(channel, message, username, user_id, avatar_url)) + #client.loop.create_task(channel.send(message)) + return flask.jsonify({'status': 'Message sent successfully.'}), 200 + else: + return flask.jsonify({'error': 'Channel not found.'}), 404 + +def run_flask_app(): + app.run(port=5001) + +# Run the Flask app in a separate thread +thread = Thread(target=run_flask_app) +thread.start() + token = "MTMyNzcxNDM3MTEyMzgxMDMwNA.GwLjEd.quGP0FA5gHRe1xLyuYq-ANuJ5cRuRQ6dhJiojI" -client.run(token) \ No newline at end of file +client.run(token) + + diff --git a/mattermost.app.py b/mattermost.app.py new file mode 100644 index 0000000..d2c2825 --- /dev/null +++ b/mattermost.app.py @@ -0,0 +1,64 @@ +import requests, json +from mattermostdriver import Driver +import bridges + +channels = { + "ibfp3fskai8adgmynbfispz3se": 1125968295967850559, #comforter + "rb43iupdy7rjbjwhg9w9c1mzjy": 954201387770736754, # lounge + "s6muherhotfoircc1yzmwr5wty": 1119502004721557554, # kweh + "fkcqa3qj83gu3bfikcu55sfwww": 1367978276185964584, #misskey + "na4doo5f83ykbc45m9a5dn513a": 1167176429797113926, #photos-from-another-star + "9ydcz9orepbtmedncb7idh43hr": 1119508652844404816, #bulletin-board + "81qmzfzeeif7mmfhpy7hkxnjuc": 955394194766192690 #photos-of-the-gang +} + +users = { + "f3nja8t9fpy73cxeh5ykzrozaw": 407247496008433675, + "3byr3scix3f78xs5bpmgqzc6pc": 189202462442389514 +} + +async def event_handler(event): + event = json.loads(event) + if 'event' in event.keys() and event['event'] == "posted": + print("event:", event) + event_data = event['data'] + post = event_data['post'] + + post = json.loads(post) + if post['channel_id'] in channels.keys(): + print(f"watched channel {post['channel_id']}") + if 'from_webhook' in post['props'].keys(): + is_webhook = post['props']['from_webhook'] + else: + is_webhook = "false" + print(is_webhook) + if is_webhook != "true": + # add file syncing means you need to get the "file_ids" key from the post and then download them into blobs and pass those along + # to the request as files, on the discord side those files would then get attached to the webhook. + + user = mattermostDriver.users.get_user(user_id=post['user_id']) + #print(user) + post['user'] = user + discord_id = None + print("user:", user) + if user['id'] in users.keys(): + discord_id = users[post['user']['id']] + post['discord_id'] = discord_id + bridges.send_mattermost_to_discord(post, channel_to=channels[post['channel_id']]) + elif post['channel_id'] == "fkcqa3qj83gu3bfikcu55sfwww" and is_webhook == "true": + username = post['props']['override_username'] + avatar_url = post['props']['override_icon_url'] + user = {"nickname": username} + post['user'] = user + post['discord_id'] = None + bridges.send_mattermost_to_discord(post, channel_to=channels[post['channel_id']], avatar_url=avatar_url) + +mattermostDriver = Driver({ + "url": "192.168.1.67", + "port": 8065, + "scheme": "http", + "token": "dmefeb8t7pditf3ot9377hrxch" +}) + +mattermostDriver.login() +mattermostDriver.init_websocket(event_handler) diff --git a/misskey.app.py b/misskey.app.py new file mode 100644 index 0000000..a7398ca --- /dev/null +++ b/misskey.app.py @@ -0,0 +1,59 @@ +import asyncio +import websockets +import misskey +import json +import requests +from io import BytesIO +import bridges + +MISSKEY_INSTANCE = "misskey.treehousefullofstars.com" +MISSKEY_TOKEN = "JSvVuz1eS2BGq6MagdQC9m109gOllwcO" + +msk = misskey.Misskey(address=MISSKEY_INSTANCE, i=MISSKEY_TOKEN) +MY_ID = msk.i()['id'] +WS_URL=f"wss://{MISSKEY_INSTANCE}/streaming" + +channels = ['localTimeline', 'globalTimeline' 'hybridTimeline', 'main'] + + +async def event_handler(event): + event = json.loads(event) + if event['body']['type'] == "note": + bridges.send_misskey_to_mattermost(event, "fkcqa3qj83gu3bfikcu55sfwww") + + +async def connect_and_listen(): + while True: + try: + print("Connecting to WebSocket...") + async with websockets.connect(WS_URL) as websocket: + print("Connected to WebSocket") + # Subscribe to the channels + for channel in channels: + subscription_request = { + "type": "connect", + "body": { + "channel": channel, + "id": MY_ID, + "access_token": MISSKEY_TOKEN + } + } + await websocket.send(json.dumps(subscription_request)) + print(f"Sent subscription request for {channel} channel with ID: {MY_ID}") + response = await websocket.recv() + print(f"Received response for {channel} channel: {response}") + await event_handler(response) + # Listen for messages + #while True: + # async for message in websocket: + # await handle_message(websocket, message) + + except websockets.exceptions.ConnectionClosedError as e: + print(f"Connection closed unexpectedly: {e}") + await asyncio.sleep(5) # Wait for 5 seconds before retrying + except Exception as e: + print(f"An error occurred: {e}") + await asyncio.sleep(5) # Wait for 5 seconds before retrying + +if __name__ == "__main__": + asyncio.run(connect_and_listen()) \ No newline at end of file diff --git a/test.json b/test.json new file mode 100644 index 0000000..fe93c47 --- /dev/null +++ b/test.json @@ -0,0 +1,117 @@ +{ + "type": "channel", + "body": { + "id": "a79jhleyz47f00j9", + "type": "note", + "body": { + "id": "a8qi6tndcsjw00uj", + "createdAt": "2025-06-08T00:42:51.673Z", + "userId": "a78vufh1z47f000t", + "user": { + "id": "a78vufh1z47f000t", + "name": "Gabriella Versi", + "username": "gabriella", + "host": null, + "avatarUrl": "https://misskey.treehousefullofstars.com/proxy/avatar.webp?url=https%3A%2F%2Fmisskey.treehousefullofstars.com%2Ffiles%2F64e5e5cf-5d64-40f1-91ab-f7858f4b0e18&avatar=1", + "avatarBlurhash": "eOPZ7PRjpd%Mx]_Nxt?bs:xZ%gWCROWCM|%gWBRjofWXSPf5xtt7V@", + "avatarDecorations": [], + "isBot": false, + "isCat": false, + "emojis": {}, + "onlineStatus": "online", + "badgeRoles": [ + { + "name": "Roots", + "iconUrl": null, + "displayOrder": 0 + } + ] + }, + "text": null, + "cw": null, + "visibility": "public", + "localOnly": false, + "reactionAcceptance": null, + "renoteCount": 0, + "repliesCount": 0, + "reactionCount": 0, + "reactions": {}, + "reactionEmojis": {}, + "reactionAndUserPairCache": [], + "fileIds": [], + "files": [], + "replyId": null, + "renoteId": "a8ohrbj431al069k", + "clippedCount": 0, + "renote": { + "id": "a8ohrbj431al069k", + "createdAt": "2025-06-06T14:55:16.000Z", + "userId": "a7xcts4vcsjw00at", + "user": { + "id": "a7xcts4vcsjw00at", + "name": "Information Is Beautiful", + "username": "infobeautiful", + "host": "vis.social", + "avatarUrl": "https://misskey.treehousefullofstars.com/proxy/avatar.webp?url=https%3A%2F%2Fcdn.masto.host%2Fvissocial%2Faccounts%2Favatars%2F111%2F030%2F299%2F829%2F248%2F466%2Foriginal%2F0d4bfae30dce0763.png&avatar=1", + "avatarBlurhash": "e2CY]zxI9xx@=y$%juXSa{aeD,ou~UV|bayCj@ena#t7yCRowIxo01", + "avatarDecorations": [], + "isBot": false, + "isCat": false, + "instance": { + "name": "vis.social", + "softwareName": "mastodon", + "softwareVersion": "4.3.8", + "iconUrl": "https://vis.social/packs/media/icons/android-chrome-36x36-4c61fdb42936428af85afdbf8c6a45a8.png", + "faviconUrl": "https://vis.social/packs/media/icons/favicon-48x48-c1197e9664ee6476d2715a1c4293bf61.png", + "themeColor": "#181820" + }, + "emojis": {}, + "onlineStatus": "unknown" + }, + "text": "Some interesting variations here...\n(by reddit user theworldmaps)", + "cw": null, + "visibility": "public", + "localOnly": false, + "reactionAcceptance": null, + "renoteCount": 1, + "repliesCount": 1, + "reactionCount": 0, + "reactions": {}, + "reactionEmojis": {}, + "reactionAndUserPairCache": [], + "emojis": {}, + "fileIds": [ + "a8ohrh7e31al069j" + ], + "files": [ + { + "id": "a8ohrh7e31al069j", + "createdAt": "2025-06-06T14:55:23.354Z", + "name": "b1743c8a716cf03a.png", + "type": "image/png", + "md5": "8fa3149c3db0e0d11e7b0b0007cabeeb", + "size": 0, + "isSensitive": false, + "blurhash": "eHQuZ@WdUE+^XoghOkXlw1oxLzNrmSr{t1vgQ;ogXOWZu2r;bwpaaM", + "properties": { + "width": 853, + "height": 1024 + }, + "url": "https://misskey.treehousefullofstars.com/files/webpublic-1225bfbb-9837-44b0-b0ac-1ed14fa1df92", + "thumbnailUrl": "https://misskey.treehousefullofstars.com/proxy/static.webp?url=https%3A%2F%2Fcdn.masto.host%2Fvissocial%2Fmedia_attachments%2Ffiles%2F114%2F636%2F994%2F245%2F080%2F080%2Foriginal%2Fb1743c8a716cf03a.png&static=1", + "comment": "A map of Europe showing the mean age of women at the birth of their first child, categorized by color from yellow (age 25) to purple (age 33). Examples include: Moldova with the youngest average age at 25.1, and Monaco with the highest at 32.5. Other notable countries: Spain at 31.6, Italy at 31.7, Germany at 29.9, and Finland at 29.9. The map includes small flags and labeled figures for precision. Data for the UK is from 2018, and other countries are from 2021 or 2022.", + "folderId": null, + "folder": null, + "userId": "a7xcts4vcsjw00at", + "user": null + } + ], + "replyId": null, + "renoteId": null, + "uri": "https://vis.social/users/infobeautiful/statuses/114636994393755424", + "url": "https://vis.social/@infobeautiful/114636994393755424", + "clippedCount": 0 + } + } + } +} \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..4377ed9 --- /dev/null +++ b/test.py @@ -0,0 +1,25 @@ +import re +import requests +from bs4 import BeautifulSoup + +# pattern = re.compile(r'!\[.*?\]\(.*?\)') +#pattern = re.compile(r'!\[.*?\]\(.*?\)') +linkmatches = re.compile(r'\b(?:https?://|www\.)\S+\b') + +test_string = "https://tenor.com/view/i-love-you-iloveyou-i-love-it-love-you-love-u-gif-1804813945664624577" + + +links = linkmatches.findall(test_string) + + +for match in links: + + response = requests.get(match) + soup = BeautifulSoup(response.text, 'html.parser') + gif_title = soup.find('meta', property='og:title')['content'] + gif_url = soup.find('meta', property='og:image')['content'] + + test_string = test_string.replace(match, "") + test_string += f"![{gif_title}]({gif_url})" + +print(test_string) \ No newline at end of file diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..a8a9406 --- /dev/null +++ b/test.txt @@ -0,0 +1 @@ +this is a test \ No newline at end of file