Created
January 16, 2026 02:25
-
-
Save AugustoPedraza/2c6d196356ecd2e1476b9fa70f6b317c to your computer and use it in GitHub Desktop.
Export account data from old Preneur app
This file contains hidden or 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
| # === REMOTE EXPORT & UPLOAD SCRIPT === | |
| # | |
| # RUN ON OLD SERVER (one line in IEx): | |
| # | |
| # username = "amadeu"; :inets.start(); :ssl.start(); {:ok, {{_, 200, _}, _, body}} = :httpc.request(:get, {~c"https://raw.githubusercontent.com/snapcall-me/life/development/priv/migration/export_and_upload.exs", []}, [], []); Code.eval_string(to_string(body), [username: username]) | |
| # | |
| # This will: | |
| # 1. Export account data to JSON | |
| # 2. Upload to Cloudinary | |
| # 3. Print the download URL for importing on staging | |
| # | |
| # ================================================================= | |
| import Ecto.Query | |
| defmodule ExportUpload do | |
| @moduledoc "Export account data and upload to Cloudinary" | |
| # Cloudinary credentials - reads from environment | |
| def cloudinary_config do | |
| %{ | |
| cloud_name: System.get_env("CLOUDINARY_CLOUD_NAME"), | |
| api_key: System.get_env("CLOUDINARY_API_KEY"), | |
| api_secret: System.get_env("CLOUDINARY_SECRET") | |
| } | |
| end | |
| def to_map(nil), do: nil | |
| def to_map(%Ecto.Association.NotLoaded{}), do: nil | |
| def to_map(%{__struct__: _} = struct) do | |
| struct | |
| |> Map.from_struct() | |
| |> Map.drop([:__meta__, :__struct__]) | |
| |> Enum.map(fn {k, v} -> {k, to_map(v)} end) | |
| |> Map.new() | |
| end | |
| def to_map(list) when is_list(list), do: Enum.map(list, &to_map/1) | |
| def to_map(tuple) when is_tuple(tuple), do: tuple |> Tuple.to_list() |> Enum.map(&to_map/1) | |
| def to_map(%{} = map), do: map |> Enum.map(fn {k, v} -> {k, to_map(v)} end) |> Map.new() | |
| def to_map(other), do: other | |
| def export(username) do | |
| IO.puts("Exporting #{username}...") | |
| account = from(a in Accounts.Account, where: a.username == ^username, preload: [:user]) | |
| |> Accounts.Repo.one() | |
| if is_nil(account) do | |
| raise "Account not found: #{username}" | |
| end | |
| contacts_ctx = from(c in Contacts.Context, where: c.snapcall_acc_id == ^account.id, limit: 1) | |
| |> Contacts.Repo.one() | |
| comm_ctx = from(c in Comm.Context, where: c.snapcall_acc_id == ^account.id, limit: 1) | |
| |> Comm.Repo.one() | |
| seg_ctx = from(c in Segmentation.Context, where: c.snapcall_acc_id == ^account.id, limit: 1) | |
| |> Segmentation.Repo.one() | |
| # Export contacts with phone numbers | |
| contacts = if contacts_ctx do | |
| raw = Contacts.Repo.preload(contacts_ctx, :contacts).contacts | |
| |> Enum.filter(fn c -> is_nil(c.deleted) or c.deleted == false end) | |
| ids = Enum.map(raw, & &1.id) | |
| phones = from(pn in Contacts.PhoneNumber, where: pn.contact_id in ^ids) | |
| |> Contacts.Repo.all() | |
| |> Enum.group_by(& &1.contact_id) | |
| Enum.map(raw, fn c -> Map.put(c, :phone_numbers, Map.get(phones, c.id, [])) end) | |
| else | |
| [] | |
| end | |
| # Export Twilio config | |
| twilio = if comm_ctx do | |
| from(pn in Comm.TwilioPhoneNumber, where: pn.context_id == ^comm_ctx.id, preload: [:twilio_account]) | |
| |> Comm.Repo.one() | |
| end | |
| # Export keywords | |
| keywords = if comm_ctx do | |
| from(k in Comm.SmsAutoReply, where: k.context_id == ^comm_ctx.id) |> Comm.Repo.all() | |
| else | |
| [] | |
| end | |
| # Export groups | |
| groups = if seg_ctx do | |
| from(g in Segmentation.Group, | |
| where: g.context_id == ^seg_ctx.id and (is_nil(g.deleted) or g.deleted == false)) | |
| |> Segmentation.Repo.all() | |
| else | |
| [] | |
| end | |
| # Export group memberships | |
| group_memberships = if contacts_ctx do | |
| from(sg in Contacts.SegmentationGrouping, | |
| where: sg.context_id == ^contacts_ctx.id, | |
| select: %{contact_id: sg.contact_id, group_id: sg.group_id}) | |
| |> Contacts.Repo.all() | |
| else | |
| [] | |
| end | |
| # Merge group IDs into contacts | |
| contacts_with_groups = Enum.map(contacts, fn contact -> | |
| group_ids = group_memberships | |
| |> Enum.filter(fn m -> m.contact_id == contact.id end) | |
| |> Enum.map(fn m -> m.group_id end) | |
| contact |> to_map() |> Map.put(:group_ids, group_ids) | |
| end) | |
| # Build export data | |
| export_data = %{ | |
| account: to_map(account), | |
| contacts: contacts_with_groups, | |
| twilio: if(twilio, do: %{ | |
| phone_number: twilio.phone_number, | |
| phone_sid: twilio.sid, | |
| twilio_account_sid: twilio.twilio_account.sid, | |
| twilio_auth_token: twilio.twilio_account.auth_token | |
| }), | |
| keywords: Enum.map(keywords, &to_map/1), | |
| groups: Enum.map(groups, fn g -> %{id: g.id, name: g.name, color: g.color} end) | |
| } | |
| json = Jason.encode!(export_data, pretty: true) | |
| IO.puts("=== EXPORT COMPLETE ===") | |
| IO.puts("Account: #{username}") | |
| IO.puts("Contacts: #{length(contacts)}") | |
| IO.puts("Groups: #{length(groups)}") | |
| IO.puts("Twilio: #{if twilio, do: "Yes", else: "No"}") | |
| IO.puts("Keywords: #{length(keywords)}") | |
| {username, json} | |
| end | |
| def upload_to_cloudinary(username, json) do | |
| config = cloudinary_config() | |
| if is_nil(config.cloud_name) or is_nil(config.api_key) or is_nil(config.api_secret) do | |
| # Save locally if no Cloudinary config | |
| filename = "/tmp/#{username}_export.json" | |
| File.write!(filename, json) | |
| IO.puts("\nNo Cloudinary config found. Saved to: #{filename}") | |
| {:local, filename} | |
| else | |
| IO.puts("\nUploading to Cloudinary...") | |
| timestamp = System.system_time(:second) |> to_string() | |
| public_id = "migration/#{username}" | |
| # Create signature | |
| sign_string = "folder=migration&public_id=#{username}×tamp=#{timestamp}#{config.api_secret}" | |
| signature = :crypto.hash(:sha, sign_string) |> Base.encode16(case: :lower) | |
| # Build multipart form | |
| boundary = "----ExportBoundary#{:rand.uniform(999999999)}" | |
| body = build_multipart_body(boundary, [ | |
| {"file", json, "#{username}_export.json"}, | |
| {"folder", "migration"}, | |
| {"public_id", username}, | |
| {"timestamp", timestamp}, | |
| {"api_key", config.api_key}, | |
| {"signature", signature} | |
| ]) | |
| url = ~c"https://api.cloudinary.com/v1_1/#{config.cloud_name}/raw/upload" | |
| content_type = ~c"multipart/form-data; boundary=#{boundary}" | |
| case :httpc.request(:post, {url, [], content_type, body}, [], []) do | |
| {:ok, {{_, 200, _}, _, response_body}} -> | |
| response = Jason.decode!(to_string(response_body)) | |
| secure_url = response["secure_url"] | |
| IO.puts("=== UPLOAD COMPLETE ===") | |
| IO.puts("URL: #{secure_url}") | |
| IO.puts("\n=== IMPORT ON STAGING ===") | |
| IO.puts("curl -o /tmp/#{username}_export.json \"#{secure_url}\"") | |
| IO.puts("Life.Migration.JsonImporter.import_from_file(\"/tmp/#{username}_export.json\")") | |
| {:ok, secure_url} | |
| {:ok, {{_, status, _}, _, response_body}} -> | |
| IO.puts("Upload failed (#{status}): #{response_body}") | |
| # Fallback to local | |
| filename = "/tmp/#{username}_export.json" | |
| File.write!(filename, json) | |
| IO.puts("Saved locally: #{filename}") | |
| {:error, status} | |
| {:error, reason} -> | |
| IO.puts("Upload error: #{inspect(reason)}") | |
| filename = "/tmp/#{username}_export.json" | |
| File.write!(filename, json) | |
| IO.puts("Saved locally: #{filename}") | |
| {:error, reason} | |
| end | |
| end | |
| end | |
| defp build_multipart_body(boundary, parts) do | |
| parts_binary = Enum.map(parts, fn | |
| {name, content, filename} -> | |
| """ | |
| --#{boundary}\r | |
| Content-Disposition: form-data; name="#{name}"; filename="#{filename}"\r | |
| Content-Type: application/json\r | |
| \r | |
| #{content}\r | |
| """ | |
| {name, value} -> | |
| """ | |
| --#{boundary}\r | |
| Content-Disposition: form-data; name="#{name}"\r | |
| \r | |
| #{value}\r | |
| """ | |
| end) | |
| |> Enum.join("") | |
| parts_binary <> "--#{boundary}--\r\n" | |
| end | |
| def run(username) do | |
| {username, json} = export(username) | |
| upload_to_cloudinary(username, json) | |
| end | |
| end | |
| # Run the export | |
| ExportUpload.run(username) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment