Skip to content

Commit

Permalink
Codebase
Browse files Browse the repository at this point in the history
  • Loading branch information
n0str committed Jun 30, 2024
1 parent 31cf2fd commit 1683dc5
Show file tree
Hide file tree
Showing 7 changed files with 403 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .env.sample
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
API_TOKEN=
BOT_TOKEN=
CODEX_BOT_TOKEN=
CHAT_ID=
37 changes: 37 additions & 0 deletions .github/workflows/build.yml
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') }}
10 changes: 10 additions & 0 deletions Dockerfile
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"]
59 changes: 59 additions & 0 deletions api.py
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
143 changes: 143 additions & 0 deletions event_parser.py
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
135 changes: 135 additions & 0 deletions main.py
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)
Loading

0 comments on commit 1683dc5

Please sign in to comment.