Created and coded bridges between misskey,

mattermost, and discord
This commit is contained in:
Jadowyne Ulve 2025-06-08 10:44:58 -05:00
parent d8521a621b
commit fea2efefc8
14 changed files with 526 additions and 6 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

189
bridges.py Normal file
View File

@ -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})

View File

@ -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)

64
mattermost.app.py Normal file
View File

@ -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)

59
misskey.app.py Normal file
View File

@ -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())

117
test.json Normal file
View File

@ -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
}
}
}
}

25
test.py Normal file
View File

@ -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)

1
test.txt Normal file
View File

@ -0,0 +1 @@
this is a test