sync_cdr.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. #!/usr/bin/env python3
  2. import asyncio
  3. import json
  4. import sqlite3
  5. from datetime import datetime, timedelta
  6. from pathlib import Path
  7. from app.config import Settings
  8. from app.threecx import ThreeCXClient
  9. DB = Path("data/telephony.sqlite3")
  10. def find_cdr_rows(value):
  11. """Findet rekursiv alle CDR-Objekte im API-Response."""
  12. rows = []
  13. if isinstance(value, dict):
  14. if "CdrId" in value:
  15. rows.append(value)
  16. else:
  17. for v in value.values():
  18. rows.extend(find_cdr_rows(v))
  19. elif isinstance(value, list):
  20. for v in value:
  21. rows.extend(find_cdr_rows(v))
  22. return rows
  23. def duration_seconds(value):
  24. if not value or not isinstance(value, str):
  25. return None
  26. # PT4M14.546215S / PT15.262243S
  27. try:
  28. value = value.removeprefix("PT")
  29. minutes = 0
  30. seconds = 0.0
  31. if "M" in value:
  32. m, value = value.split("M", 1)
  33. minutes = int(m)
  34. if value.endswith("S"):
  35. seconds = float(value[:-1])
  36. return int(round(minutes * 60 + seconds))
  37. except Exception:
  38. return None
  39. async def main():
  40. settings = Settings()
  41. client = ThreeCXClient(settings)
  42. # Letzte 7 Tage – bei Bedarf später als CLI-Parameter.
  43. today = datetime.now().date()
  44. date_to = today.isoformat()
  45. date_from = (today - timedelta(days=7)).isoformat()
  46. print(f"CDR: {date_from} -> {date_to}")
  47. result = await client.get_call_log(date_from, date_to)
  48. rows = find_cdr_rows(result)
  49. print(f"CDR-Datensätze gefunden: {len(rows)}")
  50. con = sqlite3.connect(DB)
  51. con.execute("PRAGMA foreign_keys = ON")
  52. con.execute("""
  53. CREATE TABLE IF NOT EXISTS cdr_calls (
  54. id INTEGER PRIMARY KEY AUTOINCREMENT,
  55. cdr_id TEXT NOT NULL UNIQUE,
  56. main_call_history_id TEXT,
  57. call_history_id TEXT,
  58. call_id INTEGER,
  59. segment_id INTEGER,
  60. start_time TEXT,
  61. source_dn TEXT,
  62. source_caller_id TEXT,
  63. source_display_name TEXT,
  64. destination_dn TEXT,
  65. destination_caller_id TEXT,
  66. destination_display_name TEXT,
  67. direction TEXT,
  68. call_type TEXT,
  69. status TEXT,
  70. ringing_duration TEXT,
  71. talking_duration TEXT,
  72. duration_seconds INTEGER,
  73. answered INTEGER,
  74. recording_url TEXT,
  75. src_rec_id INTEGER,
  76. dst_rec_id INTEGER,
  77. reason TEXT,
  78. raw_json TEXT NOT NULL,
  79. synced_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
  80. )
  81. """)
  82. con.execute("""
  83. CREATE INDEX IF NOT EXISTS idx_cdr_calls_start_time
  84. ON cdr_calls(start_time)
  85. """)
  86. con.execute("""
  87. CREATE INDEX IF NOT EXISTS idx_cdr_calls_source_phone
  88. ON cdr_calls(source_caller_id)
  89. """)
  90. con.execute("""
  91. CREATE INDEX IF NOT EXISTS idx_cdr_calls_destination_phone
  92. ON cdr_calls(destination_caller_id)
  93. """)
  94. con.execute("""
  95. CREATE INDEX IF NOT EXISTS idx_cdr_calls_src_rec_id
  96. ON cdr_calls(src_rec_id)
  97. """)
  98. con.execute("""
  99. CREATE INDEX IF NOT EXISTS idx_cdr_calls_dst_rec_id
  100. ON cdr_calls(dst_rec_id)
  101. """)
  102. sql = """
  103. INSERT INTO cdr_calls (
  104. cdr_id,
  105. main_call_history_id,
  106. call_history_id,
  107. call_id,
  108. segment_id,
  109. start_time,
  110. source_dn,
  111. source_caller_id,
  112. source_display_name,
  113. destination_dn,
  114. destination_caller_id,
  115. destination_display_name,
  116. direction,
  117. call_type,
  118. status,
  119. ringing_duration,
  120. talking_duration,
  121. duration_seconds,
  122. answered,
  123. recording_url,
  124. src_rec_id,
  125. dst_rec_id,
  126. reason,
  127. raw_json,
  128. synced_at
  129. )
  130. VALUES (
  131. ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
  132. ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP
  133. )
  134. ON CONFLICT(cdr_id) DO UPDATE SET
  135. main_call_history_id=excluded.main_call_history_id,
  136. call_history_id=excluded.call_history_id,
  137. call_id=excluded.call_id,
  138. segment_id=excluded.segment_id,
  139. start_time=excluded.start_time,
  140. source_dn=excluded.source_dn,
  141. source_caller_id=excluded.source_caller_id,
  142. source_display_name=excluded.source_display_name,
  143. destination_dn=excluded.destination_dn,
  144. destination_caller_id=excluded.destination_caller_id,
  145. destination_display_name=excluded.destination_display_name,
  146. direction=excluded.direction,
  147. call_type=excluded.call_type,
  148. status=excluded.status,
  149. ringing_duration=excluded.ringing_duration,
  150. talking_duration=excluded.talking_duration,
  151. duration_seconds=excluded.duration_seconds,
  152. answered=excluded.answered,
  153. recording_url=excluded.recording_url,
  154. src_rec_id=excluded.src_rec_id,
  155. dst_rec_id=excluded.dst_rec_id,
  156. reason=excluded.reason,
  157. raw_json=excluded.raw_json,
  158. synced_at=CURRENT_TIMESTAMP
  159. """
  160. for row in rows:
  161. src_rec = row.get("SrcRecId")
  162. dst_rec = row.get("DstRecId")
  163. # Nur echte Recording-IDs speichern.
  164. try:
  165. src_rec = int(src_rec) if src_rec is not None else None
  166. except (TypeError, ValueError):
  167. src_rec = None
  168. try:
  169. dst_rec = int(dst_rec) if dst_rec is not None else None
  170. except (TypeError, ValueError):
  171. dst_rec = None
  172. con.execute(sql, (
  173. row.get("CdrId"),
  174. row.get("MainCallHistoryId"),
  175. row.get("CallHistoryId"),
  176. row.get("CallId"),
  177. row.get("SegmentId"),
  178. row.get("StartTime"),
  179. row.get("SourceDn"),
  180. row.get("SourceCallerId"),
  181. row.get("SourceDisplayName"),
  182. row.get("DestinationDn"),
  183. row.get("DestinationCallerId"),
  184. row.get("DestinationDisplayName"),
  185. row.get("Direction"),
  186. row.get("CallType"),
  187. row.get("Status"),
  188. row.get("RingingDuration"),
  189. row.get("TalkingDuration"),
  190. duration_seconds(row.get("TalkingDuration")),
  191. 1 if row.get("Answered") else 0,
  192. row.get("RecordingUrl"),
  193. src_rec,
  194. dst_rec,
  195. row.get("Reason"),
  196. json.dumps(row, ensure_ascii=False),
  197. ))
  198. con.commit()
  199. count = con.execute(
  200. "SELECT COUNT(*) FROM cdr_calls"
  201. ).fetchone()[0]
  202. recordings = con.execute("""
  203. SELECT COUNT(*)
  204. FROM cdr_calls
  205. WHERE src_rec_id IS NOT NULL
  206. OR dst_rec_id IS NOT NULL
  207. """).fetchone()[0]
  208. print()
  209. print("=== CDR SYNCHRONISIERT ===")
  210. print("Gespeicherte CDRs:", count)
  211. print("Mit Recording:", recordings)
  212. print()
  213. print("=== LETZTE RECORDINGS ===")
  214. for row in con.execute("""
  215. SELECT
  216. start_time,
  217. source_caller_id,
  218. destination_caller_id,
  219. direction,
  220. status,
  221. src_rec_id,
  222. recording_url
  223. FROM cdr_calls
  224. WHERE recording_url IS NOT NULL
  225. ORDER BY start_time DESC
  226. LIMIT 10
  227. """):
  228. print(row)
  229. con.close()
  230. if __name__ == "__main__":
  231. asyncio.run(main())