-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
n0str
committed
Jun 30, 2024
1 parent
31cf2fd
commit 1683dc5
Showing
7 changed files
with
403 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
API_TOKEN= | ||
BOT_TOKEN= | ||
CODEX_BOT_TOKEN= | ||
CHAT_ID= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
name: Build and push docker image | ||
|
||
on: [push] | ||
|
||
env: | ||
DOCKER_REPO: codex-team/codex-figma-notify | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-20.04 | ||
|
||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@v2 | ||
|
||
- name: Login to GitHub Container Registry | ||
uses: docker/login-action@v1 | ||
with: | ||
registry: ghcr.io | ||
username: ${{ github.actor }} | ||
password: ${{ secrets.GITHUB_TOKEN }} | ||
|
||
- name: Build and push production image | ||
if: endsWith(github.ref, '/prod') | ||
uses: docker/build-push-action@v2 | ||
with: | ||
context: . | ||
tags: ghcr.io/${{ env.DOCKER_REPO }}:prod | ||
push: ${{ endsWith(github.ref, '/prod') }} | ||
|
||
- name: Build and push stage image | ||
if: endsWith(github.ref, '/stage') | ||
uses: docker/build-push-action@v2 | ||
with: | ||
context: . | ||
tags: ghcr.io/${{ env.DOCKER_REPO }}:stage | ||
push: ${{ endsWith(github.ref, '/stage') }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
FROM python:3.8.7-slim-buster | ||
|
||
WORKDIR /usr/src/app | ||
|
||
COPY requirements.txt . | ||
RUN pip install -r requirements.txt | ||
|
||
COPY . . | ||
ENTRYPOINT ["python"] | ||
CMD ["main.py"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import os | ||
import requests | ||
from dotenv import load_dotenv | ||
|
||
class FigmaAPI: | ||
|
||
API_V1 = 'https://api.figma.com/v1' | ||
FIGMA_WEB = 'https://www.figma.com/design' | ||
|
||
def __init__(self, token, limit=5): | ||
self._token = token | ||
self.limit = limit | ||
self.headers = { | ||
'X-FIGMA-TOKEN': self._token, | ||
} | ||
self.timeout = 10 | ||
|
||
@staticmethod | ||
def load_from_env(): | ||
load_dotenv(override=True) | ||
token = os.getenv('API_TOKEN') | ||
limit = int(os.getenv('LIMIT', 10)) | ||
return FigmaAPI(token=token, limit=limit) | ||
|
||
def get_node_url_from_component(self, component): | ||
meta = component['meta'] | ||
return f"{FigmaAPI.FIGMA_WEB}/{meta['file_key']}?node-id={meta['node_id'].replace(':', '-')}" | ||
|
||
def get_component_info(self, key): | ||
r = requests.get(f"{FigmaAPI.API_V1}/components/{key}", headers=self.headers, timeout=self.timeout) | ||
if r.status_code != 200: | ||
print(f"Cannot get info of {key} component: {r.text}") | ||
return None | ||
else: | ||
return r.json() | ||
|
||
def get_versions(self, key): | ||
r = requests.get(f"{FigmaAPI.API_V1}/files/{key}/versions", headers=self.headers, timeout=self.timeout) | ||
if r.status_code != 200: | ||
print(f"Cannot get info of {key} versions: {r.text}") | ||
return None | ||
else: | ||
return list(map(lambda x: x['id'], filter(lambda x: x['label'] is not None, r.json()["versions"])))[:2] | ||
|
||
def get_history(self, key, ids, version): | ||
r = requests.get(f"{FigmaAPI.API_V1}/files/{key}?ids={','.join(ids)}&version={version}", headers=self.headers, timeout=self.timeout) | ||
if r.status_code != 200: | ||
print(f"Cannot get history of {key} for ids={ids} version: {version}") | ||
return None | ||
else: | ||
return r.json() | ||
|
||
class Component: | ||
|
||
def __init__(self, data) -> None: | ||
self.data = data | ||
|
||
def __str__(self) -> str: | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import json | ||
import re | ||
from api import FigmaAPI | ||
from jycm.jycm import YouchamaJsonDiffer | ||
|
||
class FigmaEventParser: | ||
|
||
NodeIDRegex = r'(\d+[:-]\d+)' | ||
|
||
def __init__(self, event, figma_api) -> None: | ||
self.event = event | ||
self.api = figma_api | ||
self.message_lines = [] | ||
|
||
@staticmethod | ||
def convert_node_id(node_id): | ||
return node_id.replace(':', '-') | ||
|
||
@staticmethod | ||
def get_node_url_from_component(component): | ||
meta = component['meta'] | ||
return f"{FigmaAPI.FIGMA_WEB}/{meta['file_key']}?node-id={FigmaEventParser.convert_node_id(meta['node_id'])}" | ||
|
||
def modified_components(self): | ||
modified_info = { | ||
'component_ids': set(), | ||
'components': [] | ||
} | ||
component_ids = set() | ||
if 'modified_components' in self.event: | ||
for component in self.event['modified_components']: | ||
key = component['key'] | ||
info = self.api.get_component_info(key) | ||
meta = info['meta'] | ||
node_id = FigmaEventParser.convert_node_id(meta['node_id']) | ||
component_ids.add(node_id) | ||
|
||
if 'name' in meta['containing_frame']: | ||
name = f"{meta['containing_frame']['name']}, {meta['name']}" | ||
else: | ||
name = meta['name'] | ||
|
||
modified_info['components'].append([node_id, name]) | ||
|
||
modified_info['component_ids'] = component_ids | ||
return modified_info | ||
|
||
def created_components(self): | ||
created_info = { | ||
'component_ids': set(), | ||
'components': [] | ||
} | ||
component_ids = set() | ||
if 'created_components' in self.event: | ||
for component in self.event['created_components']: | ||
key = component['key'] | ||
info = self.api.get_component_info(key) | ||
meta = info['meta'] | ||
component_ids.add(FigmaEventParser.convert_node_id(meta['node_id'])) | ||
|
||
if 'name' in meta['containing_frame']: | ||
name = f"{meta['containing_frame']['name']}, {meta['name']}" | ||
else: | ||
name = meta['name'] | ||
|
||
created_info['components'].append([meta['node_id'], name]) | ||
|
||
created_info['component_ids'] = component_ids | ||
return created_info | ||
|
||
@staticmethod | ||
def traverse_path(obj, path): | ||
new_path = [] | ||
cur = obj | ||
for key in path.split("->"): | ||
if key == "": | ||
return "" | ||
elif key[0]=='[' and key[-1]==']': | ||
cur = cur[int(key[1:-1])] | ||
if 'name' in cur: | ||
new_path.append(f"({cur['name']})") | ||
elif isinstance(cur, str): | ||
new_path.append(cur) | ||
else: | ||
new_path.append(f"ERROR") | ||
else: | ||
cur=cur[key] | ||
new_path.append(key) | ||
return ' -> '.join(new_path) | ||
|
||
@staticmethod | ||
def flatten_object(obj, path): | ||
if isinstance(obj, str): | ||
return obj | ||
else: | ||
if 'componentSetId' in obj: | ||
node_id = obj['componentSetId'] | ||
elif 'id' in obj: | ||
node_id = obj['id'] | ||
else: | ||
node_id = re.findall(FigmaEventParser.NodeIDRegex, path)[-1] | ||
return [FigmaEventParser.convert_node_id(node_id), obj['name']] | ||
|
||
def generate_history_diff(self, left, right): | ||
ycm = YouchamaJsonDiffer(left, right) | ||
ycm.diff() | ||
diff_result = ycm.to_dict(no_pairs=True) | ||
with open("diff.json", "w") as w: | ||
w.write(json.dumps(diff_result)) | ||
|
||
if 'value_changes' in diff_result: | ||
for i, change in enumerate(diff_result['value_changes']): | ||
diff_result['value_changes'][i]['path'] = FigmaEventParser.traverse_path(left, change['left_path']) | ||
del diff_result['value_changes'][i]['left'] | ||
del diff_result['value_changes'][i]['right'] | ||
del diff_result['value_changes'][i]['left_path'] | ||
del diff_result['value_changes'][i]['right_path'] | ||
else: | ||
diff_result['value_changes'] = [] | ||
|
||
if 'dict:add' in diff_result: | ||
for i, change in enumerate(diff_result['dict:add']): | ||
diff_result['dict:add'][i]['path'] = FigmaEventParser.traverse_path(right, change['right_path']) | ||
diff_result['dict:add'][i]['value'] = FigmaEventParser.flatten_object(change['right'], change['right_path']) | ||
del diff_result['dict:add'][i]['left'] | ||
del diff_result['dict:add'][i]['right'] | ||
del diff_result['dict:add'][i]['left_path'] | ||
del diff_result['dict:add'][i]['right_path'] | ||
else: | ||
diff_result['dict:add'] = [] | ||
|
||
if 'list:add' in diff_result: | ||
for i, change in enumerate(diff_result['list:add']): | ||
diff_result['list:add'][i]['path'] = FigmaEventParser.traverse_path(right, change['right_path']) | ||
diff_result['list:add'][i]['value'] = FigmaEventParser.flatten_object(change['right'], change['right_path']) | ||
del diff_result['list:add'][i]['left'] | ||
del diff_result['list:add'][i]['right'] | ||
del diff_result['list:add'][i]['left_path'] | ||
del diff_result['list:add'][i]['right_path'] | ||
else: | ||
diff_result['list:add'] = [] | ||
|
||
return diff_result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import json | ||
import requests | ||
import re | ||
from api import FigmaAPI | ||
from event_parser import FigmaEventParser | ||
from flask import Flask, request | ||
import json | ||
import os | ||
from datetime import datetime | ||
import random | ||
|
||
app = Flask(__name__) | ||
limit = 10 | ||
changes_limit = 10 | ||
figma_api = FigmaAPI.load_from_env() | ||
|
||
def generate_report(event, created, modified, history_diff): | ||
def insert_link_to_path(path): | ||
nodes = re.findall(r'(\d+[-:]\d+)', path) | ||
for node in nodes: | ||
if node in components_dict: | ||
path = path.replace(node, f"<a href='{FigmaAPI.FIGMA_WEB}/{file_key}?node-id={node}'>{components_dict[node]}</a>") | ||
return path | ||
lines = [] | ||
file_key = event['file_key'] | ||
|
||
components_dict = {} | ||
for obj in created: | ||
components_dict[obj[0]] = obj[1] | ||
components_dict[obj[0].replace('-', ':')] = obj[1] | ||
for obj in modified: | ||
components_dict[obj[0]] = obj[1] | ||
components_dict[obj[0].replace('-', ':')] = obj[1] | ||
|
||
lines.append(f"🛍 {event['triggered_by']['handle']} published <a href='{FigmaAPI.FIGMA_WEB}/{file_key}'>{event['file_name']} library</a>") | ||
|
||
if event['description'] != "": | ||
lines.append(f"\n<blockquote>{event['description']}</blockquote>") | ||
|
||
if len(created): | ||
lines.append("\n<b>Created components</b>\n") | ||
for component in created[:limit]: | ||
lines.append(f"<a href='{FigmaAPI.FIGMA_WEB}/{file_key}?node-id={component[0]}'>{component[1]}</a>") | ||
if len(created) > limit: | ||
lines.append(f"<u>and {len(created)-limit} more</u>") | ||
|
||
if len(modified): | ||
lines.append("\n<b>Modified components</b>\n") | ||
for component in modified[:limit]: | ||
lines.append(f"<a href='{FigmaAPI.FIGMA_WEB}/{file_key}?node-id={component[0]}'>{component[1]}</a>") | ||
if len(modified) > limit: | ||
lines.append(f"<u>and {len(modified)-limit} more</u>") | ||
|
||
lines.append(f"\n<b>Changes</b>\n") | ||
|
||
changes = [] | ||
for component in history_diff['dict:add']: | ||
component["path"] = insert_link_to_path(component["path"]) | ||
changes.append(f"- Add <a href='{FigmaAPI.FIGMA_WEB}/{file_key}?node-id={component['value'][0]}'>{component['value'][1]}</a> on path <i>{component['path']}</i>") | ||
|
||
for component in history_diff['list:add']: | ||
component["path"] = insert_link_to_path(component["path"]) | ||
if isinstance(component['value'], str): | ||
changes.append(f"- Set {component['value']} on path <i>{component['path']}</i>") | ||
else: | ||
changes.append(f"- Set <a href='{FigmaAPI.FIGMA_WEB}/{file_key}?node-id={component['value'][0]}'>{component['value'][1]}</a> on path <i>{component['path']}</i>") | ||
for component in history_diff['value_changes']: | ||
component["path"] = insert_link_to_path(component["path"]) | ||
if component['path'] == 'thumbnailUrl': | ||
continue | ||
changes.append(f"- Change <i>{component['path']}</i> from {component['old']} to {component['new']}") | ||
|
||
lines.extend(changes[:changes_limit]) | ||
if len(changes) > changes_limit: | ||
lines.append(f"<u>and {len(changes)-changes_limit} more</u>") | ||
|
||
return "\n".join(lines) | ||
|
||
|
||
@app.route('/', methods=['POST']) | ||
def store_json(): | ||
# Get the incoming JSON data | ||
data = request.get_json() | ||
if not data: | ||
return "No JSON data received", 400 | ||
|
||
# Generate a filename with the current timestamp | ||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S') | ||
prefix = f"{timestamp}-{random.randint(1000, 10000)}" | ||
filename = f"{prefix}-event.txt" | ||
|
||
# Save the JSON data to a file | ||
with open(filename, 'w') as file: | ||
json.dump(data, file) | ||
|
||
event = data | ||
event_parser = FigmaEventParser(event, figma_api) | ||
|
||
created = event_parser.created_components() | ||
modified = event_parser.modified_components() | ||
|
||
component_ids = created['component_ids'] | modified['component_ids'] | ||
|
||
if 'file_key' in event: | ||
versions = figma_api.get_versions(event['file_key']) | ||
history_new = figma_api.get_history(event['file_key'], list(component_ids), versions[0]) | ||
history_old = figma_api.get_history(event['file_key'], list(component_ids), versions[1]) | ||
|
||
with open(f"{prefix}-new.json", "w") as w: | ||
w.write(json.dumps(history_new)) | ||
|
||
with open(f"{prefix}-old.json", "w") as w: | ||
w.write(json.dumps(history_old)) | ||
|
||
diff_report = event_parser.generate_history_diff(history_old, history_new) | ||
|
||
with open("{prefix}-diff.json", "w") as w: | ||
w.write(json.dumps(diff_report)) | ||
|
||
message = generate_report(event, created['components'], modified['components'], diff_report) | ||
with open("{prefix}-message.txt", "w") as w: | ||
w.write(message) | ||
|
||
CODEX_BOT_TOKEN = os.environ.get('CODEX_BOT_TOKEN', None) | ||
if CODEX_BOT_TOKEN is not None: | ||
requests.post(f"https://notify.bot.codex.so/u/{CODEX_BOT_TOKEN}", data={ | ||
"message": message, | ||
"parse_mode": "HTML" | ||
}) | ||
|
||
return "JSON data saved", 200 | ||
|
||
|
||
if __name__ == '__main__': | ||
app.run(host="0.0.0.0",port=80) |
Oops, something went wrong.