Compare commits
2 Commits
main
..
81c755ec3a
| Author | SHA1 | Date | |
|---|---|---|---|
| 81c755ec3a | |||
| ef711d461f |
@@ -1,33 +0,0 @@
|
|||||||
# Proxy Finder Module
|
|
||||||
|
|
||||||
## find_anon_proxies.py — Поиск анонимных прокси
|
|
||||||
|
|
||||||
### Использование:
|
|
||||||
```bash
|
|
||||||
python3 /root/.openclaw/workspace/scripts/proxy-finder/find_anon_proxies.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Что делает:
|
|
||||||
1. Загружает прокси из 6 источников (proxyscrape, spys.me, sslproxies.org и др.)
|
|
||||||
2. Парсит IP:PORT форматы
|
|
||||||
3. Фильтрует локальные/резервированные адреса
|
|
||||||
4. Проверяет работоспособность через curl
|
|
||||||
5. Тестирует анонимность (проверка X-Forwarded-For)
|
|
||||||
|
|
||||||
### Результат:
|
|
||||||
Сохраняется в `anonymous_proxies.txt` — формат: `IP:PORT (HTTP/SOCKS5)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Использование прокси для запросов:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# HTTP-запрос через прокси
|
|
||||||
curl -s --proxy http://PROXY_IP:PORT https://example.com
|
|
||||||
|
|
||||||
# SOCKS5-запрос через прокси
|
|
||||||
curl -s --socks5 PROXY_IP:PORT https://example.com
|
|
||||||
|
|
||||||
# Python requests с прокси
|
|
||||||
python3 -c "import requests; r = requests.get('https://example.com', proxies={'http': 'socks5://PROXY_IP:PORT'}); print(r.text)"
|
|
||||||
```
|
|
||||||
@@ -1,480 +0,0 @@
|
|||||||
101.255.209.93:8090
|
|
||||||
162.240.19.30:80
|
|
||||||
85.117.56.85:8080
|
|
||||||
163.172.129.144:3128
|
|
||||||
97.213.76.123:80
|
|
||||||
197.221.234.149:80
|
|
||||||
138.201.198.53:80
|
|
||||||
154.66.159.34:800
|
|
||||||
38.127.172.7:37234
|
|
||||||
176.115.146.232:8080
|
|
||||||
191.37.33.38:42999
|
|
||||||
110.164.175.110:8080
|
|
||||||
165.154.7.156:8888
|
|
||||||
103.172.17.51:8080
|
|
||||||
185.128.240.2:3128
|
|
||||||
45.232.152.2:8080
|
|
||||||
103.13.204.84:8082
|
|
||||||
103.85.183.30:4995
|
|
||||||
43.133.187.166:3128
|
|
||||||
186.194.62.202:999
|
|
||||||
82.115.60.51:80
|
|
||||||
176.94.224.86:8080
|
|
||||||
43.167.199.44:3128
|
|
||||||
190.60.34.250:999
|
|
||||||
49.148.47.13:8080
|
|
||||||
147.231.163.133:80
|
|
||||||
38.19.42.176:999
|
|
||||||
117.55.203.165:8899
|
|
||||||
207.246.234.115:4669
|
|
||||||
150.136.163.51:80
|
|
||||||
4.233.138.204:8888
|
|
||||||
207.180.254.198:8080
|
|
||||||
157.66.16.38:8070
|
|
||||||
217.160.39.73:18080
|
|
||||||
124.106.83.244:8083
|
|
||||||
77.235.31.24:8080
|
|
||||||
163.172.167.48:80
|
|
||||||
113.11.37.81:2505
|
|
||||||
43.255.159.94:3129
|
|
||||||
167.88.164.6:3129
|
|
||||||
79.76.121.87:3128
|
|
||||||
169.239.208.70:8080
|
|
||||||
103.10.228.119:8080
|
|
||||||
12.50.107.217:80
|
|
||||||
157.230.241.91:3128
|
|
||||||
149.28.137.172:8888
|
|
||||||
210.79.146.82:8085
|
|
||||||
103.1.51.177:8181
|
|
||||||
103.124.199.116:8080
|
|
||||||
54.38.35.209:3128
|
|
||||||
103.17.215.9:8089
|
|
||||||
206.189.144.164:10808
|
|
||||||
178.128.59.180:18080
|
|
||||||
42.116.10.196:443
|
|
||||||
70.35.196.194:8082
|
|
||||||
5.101.5.160:2080
|
|
||||||
176.88.166.165:8080
|
|
||||||
38.171.255.232:999
|
|
||||||
103.170.22.145:8080
|
|
||||||
103.19.58.134:8080
|
|
||||||
3.110.246.113:3128
|
|
||||||
103.47.13.41:8080
|
|
||||||
172.93.101.178:3128
|
|
||||||
103.125.38.50:8080
|
|
||||||
103.108.146.142:8080
|
|
||||||
45.139.91.62:8080
|
|
||||||
38.75.81.10:999
|
|
||||||
160.19.19.170:3125
|
|
||||||
103.224.55.154:8090
|
|
||||||
115.187.29.25:89
|
|
||||||
129.151.130.247:1111
|
|
||||||
178.156.206.253:8118
|
|
||||||
45.4.202.147:999
|
|
||||||
103.84.228.12:3128
|
|
||||||
177.139.174.81:8080
|
|
||||||
181.37.240.89:999
|
|
||||||
103.129.127.244:8088
|
|
||||||
202.51.106.229:8080
|
|
||||||
154.126.213.152:8070
|
|
||||||
210.16.85.42:8080
|
|
||||||
103.153.190.43:8080
|
|
||||||
38.199.67.10:999
|
|
||||||
103.156.248.102:8080
|
|
||||||
103.44.2.177:8080
|
|
||||||
84.22.42.41:8080
|
|
||||||
103.173.141.10:8080
|
|
||||||
170.245.132.81:999
|
|
||||||
104.152.50.252:8000
|
|
||||||
36.92.104.123:8000
|
|
||||||
37.35.65.190:8080
|
|
||||||
103.84.177.28:8083
|
|
||||||
38.226.241.242:8080
|
|
||||||
194.180.188.100:8080
|
|
||||||
103.29.239.132:8080
|
|
||||||
77.240.97.77:8080
|
|
||||||
103.231.236.235:8182
|
|
||||||
27.116.41.156:8470
|
|
||||||
103.156.74.209:3125
|
|
||||||
38.172.128.139:999
|
|
||||||
51.161.137.166:8080
|
|
||||||
45.231.221.129:999
|
|
||||||
103.137.218.166:83
|
|
||||||
38.188.186.47:999
|
|
||||||
187.62.65.5:8080
|
|
||||||
103.68.215.46:8080
|
|
||||||
210.177.178.148:80
|
|
||||||
157.10.184.115:8080
|
|
||||||
79.106.33.26:8079
|
|
||||||
152.228.145.27:8080
|
|
||||||
190.103.205.253:9097
|
|
||||||
41.59.90.171:80
|
|
||||||
186.216.208.98:3128
|
|
||||||
84.244.119.36:23500
|
|
||||||
38.188.48.65:8080
|
|
||||||
43.133.30.18:3128
|
|
||||||
101.255.157.6:8080
|
|
||||||
131.222.247.180:8080
|
|
||||||
103.131.232.9:8080
|
|
||||||
103.209.36.58:8080
|
|
||||||
200.8.200.191:8088
|
|
||||||
190.83.40.166:3128
|
|
||||||
156.232.99.66:10808
|
|
||||||
131.222.247.238:8080
|
|
||||||
82.39.154.145:8080
|
|
||||||
201.222.50.218:80
|
|
||||||
124.106.223.156:9999
|
|
||||||
65.108.103.19:80
|
|
||||||
221.120.208.25:8080
|
|
||||||
196.1.93.10:80
|
|
||||||
194.14.207.87:80
|
|
||||||
103.204.46.130:8090
|
|
||||||
103.173.214.187:8080
|
|
||||||
74.62.179.122:8080
|
|
||||||
75.84.71.14:80
|
|
||||||
197.221.240.176:80
|
|
||||||
157.100.12.154:999
|
|
||||||
102.36.160.95:8080
|
|
||||||
103.125.155.230:8080
|
|
||||||
190.26.209.126:999
|
|
||||||
178.156.224.42:3128
|
|
||||||
79.110.192.44:8081
|
|
||||||
186.96.15.70:8080
|
|
||||||
196.251.223.54:8080
|
|
||||||
103.158.96.77:8080
|
|
||||||
181.143.145.98:8080
|
|
||||||
165.0.136.30:8080
|
|
||||||
103.180.123.103:8090
|
|
||||||
103.175.240.65:7777
|
|
||||||
103.25.220.22:8081
|
|
||||||
161.49.87.222:8095
|
|
||||||
45.188.167.25:999
|
|
||||||
103.164.171.210:8080
|
|
||||||
154.9.30.1:50000
|
|
||||||
43.153.199.126:8888
|
|
||||||
103.118.44.33:8080
|
|
||||||
175.143.19.216:8081
|
|
||||||
103.99.27.43:3125
|
|
||||||
180.191.2.165:8081
|
|
||||||
103.247.14.222:8080
|
|
||||||
185.65.247.133:48049
|
|
||||||
181.143.42.138:8080
|
|
||||||
197.221.249.196:80
|
|
||||||
135.125.154.101:8899
|
|
||||||
91.122.216.160:8080
|
|
||||||
34.143.154.126:8888
|
|
||||||
103.120.175.243:9191
|
|
||||||
103.191.171.142:8181
|
|
||||||
103.13.192.76:8080
|
|
||||||
103.227.187.11:6090
|
|
||||||
139.255.5.98:443
|
|
||||||
103.242.106.155:3125
|
|
||||||
190.97.228.67:999
|
|
||||||
43.133.1.198:3128
|
|
||||||
43.167.214.133:3128
|
|
||||||
43.245.93.193:53805
|
|
||||||
103.152.239.127:3125
|
|
||||||
192.248.95.98:54126
|
|
||||||
138.252.158.7:8080
|
|
||||||
168.243.77.190:999
|
|
||||||
103.187.86.10:8182
|
|
||||||
103.51.223.133:8080
|
|
||||||
138.121.114.54:8080
|
|
||||||
85.117.61.108:8080
|
|
||||||
208.67.28.19:58090
|
|
||||||
43.229.79.190:8080
|
|
||||||
149.86.206.27:8080
|
|
||||||
139.135.182.132:8081
|
|
||||||
168.228.176.12:3139
|
|
||||||
161.49.90.70:1337
|
|
||||||
114.8.131.181:8080
|
|
||||||
177.234.194.234:999
|
|
||||||
103.180.126.236:8080
|
|
||||||
141.136.13.51:8080
|
|
||||||
180.180.218.250:8080
|
|
||||||
164.90.223.123:3128
|
|
||||||
213.131.85.30:1976
|
|
||||||
143.208.84.2:8589
|
|
||||||
103.126.87.120:8082
|
|
||||||
47.52.223.161:5872
|
|
||||||
103.192.174.154:8080
|
|
||||||
103.175.236.180:8382
|
|
||||||
149.28.87.103:8888
|
|
||||||
212.231.191.23:80
|
|
||||||
122.117.203.252:3128
|
|
||||||
108.161.135.118:80
|
|
||||||
103.106.79.98:8080
|
|
||||||
45.179.200.38:999
|
|
||||||
103.112.131.14:8080
|
|
||||||
103.194.175.51:7777
|
|
||||||
45.167.126.21:999
|
|
||||||
103.111.225.106:8080
|
|
||||||
34.43.46.91:80
|
|
||||||
61.19.145.66:8080
|
|
||||||
45.224.23.238:999
|
|
||||||
38.54.116.154:3128
|
|
||||||
190.60.60.37:8080
|
|
||||||
43.133.22.248:9091
|
|
||||||
103.41.88.182:84
|
|
||||||
45.189.252.18:999
|
|
||||||
86.127.243.197:80
|
|
||||||
181.188.216.3:8080
|
|
||||||
222.252.14.70:8443
|
|
||||||
109.224.242.151:8080
|
|
||||||
85.214.107.177:80
|
|
||||||
70.61.188.34:3128
|
|
||||||
51.81.6.158:3128
|
|
||||||
217.162.8.134:80
|
|
||||||
103.80.83.27:8080
|
|
||||||
43.153.182.147:3128
|
|
||||||
193.38.224.169:8081
|
|
||||||
177.234.226.83:1994
|
|
||||||
119.2.45.81:3125
|
|
||||||
187.102.211.253:999
|
|
||||||
89.28.81.217:8443
|
|
||||||
103.156.17.139:8818
|
|
||||||
64.49.15.225:8443
|
|
||||||
38.19.111.74:8080
|
|
||||||
201.140.209.33:3128
|
|
||||||
103.3.59.208:8080
|
|
||||||
204.157.251.213:999
|
|
||||||
112.198.138.14:8082
|
|
||||||
113.192.30.27:7777
|
|
||||||
103.15.214.70:8080
|
|
||||||
103.1.93.184:55443
|
|
||||||
110.34.13.4:8080
|
|
||||||
81.0.49.104:20500
|
|
||||||
180.195.60.98:8081
|
|
||||||
103.145.34.100:1111
|
|
||||||
161.49.215.28:10101
|
|
||||||
202.138.240.249:8080
|
|
||||||
192.203.0.250:999
|
|
||||||
103.144.102.60:8181
|
|
||||||
124.121.186.200:8080
|
|
||||||
85.214.204.79:80
|
|
||||||
185.231.59.189:8080
|
|
||||||
110.34.1.178:7777
|
|
||||||
45.174.77.1:999
|
|
||||||
180.191.233.18:5050
|
|
||||||
103.154.77.46:1111
|
|
||||||
180.191.254.10:8081
|
|
||||||
103.165.155.195:8080
|
|
||||||
157.20.157.24:8080
|
|
||||||
122.3.145.194:8083
|
|
||||||
45.65.227.161:999
|
|
||||||
87.106.120.212:3128
|
|
||||||
186.125.6.241:8080
|
|
||||||
43.167.187.107:3128
|
|
||||||
103.209.38.132:8080
|
|
||||||
43.167.245.99:3128
|
|
||||||
82.22.184.158:3128
|
|
||||||
119.93.83.106:8082
|
|
||||||
124.104.197.204:8080
|
|
||||||
183.110.216.128:8090
|
|
||||||
167.99.124.118:80
|
|
||||||
102.0.25.184:8080
|
|
||||||
51.79.71.202:8080
|
|
||||||
45.198.8.204:8080
|
|
||||||
103.178.21.104:3125
|
|
||||||
186.250.202.104:8080
|
|
||||||
45.32.69.101:9000
|
|
||||||
103.43.191.71:8888
|
|
||||||
106.0.158.114:8080
|
|
||||||
103.164.214.122:8080
|
|
||||||
124.156.230.244:3128
|
|
||||||
103.61.16.20:8780
|
|
||||||
206.135.55.224:999
|
|
||||||
103.26.176.25:8080
|
|
||||||
103.245.110.198:1452
|
|
||||||
43.133.169.167:3128
|
|
||||||
181.204.185.98:8081
|
|
||||||
103.171.194.52:84
|
|
||||||
125.209.110.83:39617
|
|
||||||
185.219.86.12:8080
|
|
||||||
105.22.37.218:8080
|
|
||||||
50.114.33.3:8080
|
|
||||||
62.90.70.144:7443
|
|
||||||
45.224.23.235:999
|
|
||||||
103.169.38.66:8080
|
|
||||||
45.155.102.216:3128
|
|
||||||
181.115.67.3:999
|
|
||||||
174.104.115.21:80
|
|
||||||
104.161.23.122:5003
|
|
||||||
45.169.148.2:999
|
|
||||||
43.160.246.46:3128
|
|
||||||
103.118.224.19:5678
|
|
||||||
154.17.8.103:1680
|
|
||||||
190.110.226.122:80
|
|
||||||
142.147.119.181:8080
|
|
||||||
189.193.225.86:999
|
|
||||||
114.111.151.41:80
|
|
||||||
190.61.63.106:8080
|
|
||||||
46.203.233.116:3128
|
|
||||||
103.67.85.150:3127
|
|
||||||
103.49.166.193:83
|
|
||||||
176.99.134.183:8090
|
|
||||||
180.232.171.210:8080
|
|
||||||
45.240.232.62:8080
|
|
||||||
105.27.130.22:9812
|
|
||||||
176.12.65.24:443
|
|
||||||
190.97.231.0:999
|
|
||||||
103.110.10.162:1992
|
|
||||||
176.61.151.123:80
|
|
||||||
103.133.24.73:8787
|
|
||||||
89.43.134.35:8080
|
|
||||||
163.227.146.17:8181
|
|
||||||
47.236.86.147:443
|
|
||||||
201.140.185.41:8081
|
|
||||||
38.43.93.161:8000
|
|
||||||
38.194.251.246:999
|
|
||||||
45.179.244.8:999
|
|
||||||
200.215.229.33:999
|
|
||||||
194.58.42.190:3128
|
|
||||||
36.95.208.10:8080
|
|
||||||
149.62.230.50:8080
|
|
||||||
103.231.239.137:58080
|
|
||||||
18.169.141.215:3128
|
|
||||||
60.249.149.98:8080
|
|
||||||
190.97.253.233:999
|
|
||||||
156.155.50.93:8080
|
|
||||||
210.61.216.63:60808
|
|
||||||
165.227.169.229:3080
|
|
||||||
203.146.80.235:8080
|
|
||||||
103.97.140.64:8080
|
|
||||||
41.184.92.220:80
|
|
||||||
43.167.213.156:3128
|
|
||||||
45.239.48.98:999
|
|
||||||
51.178.253.98:80
|
|
||||||
160.19.19.100:8080
|
|
||||||
109.199.125.66:3128
|
|
||||||
101.255.208.18:8090
|
|
||||||
190.131.205.150:999
|
|
||||||
103.76.12.158:8080
|
|
||||||
104.194.148.188:3128
|
|
||||||
45.89.82.210:8080
|
|
||||||
103.229.164.178:3128
|
|
||||||
49.0.1.60:8080
|
|
||||||
38.199.71.79:999
|
|
||||||
212.67.31.235:8080
|
|
||||||
95.78.161.82:7777
|
|
||||||
102.164.255.155:8080
|
|
||||||
157.20.252.7:8097
|
|
||||||
118.163.13.200:8080
|
|
||||||
43.251.253.40:8080
|
|
||||||
92.255.248.78:8080
|
|
||||||
104.248.156.141:8888
|
|
||||||
160.19.16.101:8181
|
|
||||||
143.198.135.176:80
|
|
||||||
103.102.153.215:33128
|
|
||||||
38.127.172.219:37234
|
|
||||||
150.107.136.205:39843
|
|
||||||
13.114.160.78:80
|
|
||||||
101.255.209.182:1111
|
|
||||||
45.225.89.145:999
|
|
||||||
38.211.24.242:8080
|
|
||||||
177.234.247.238:999
|
|
||||||
14.240.238.209:8080
|
|
||||||
95.3.69.222:8080
|
|
||||||
20.110.224.54:3128
|
|
||||||
165.232.119.106:3128
|
|
||||||
103.76.109.167:80
|
|
||||||
103.102.14.64:666
|
|
||||||
68.183.139.61:3128
|
|
||||||
41.184.92.219:80
|
|
||||||
103.242.105.58:8080
|
|
||||||
152.32.68.171:65535
|
|
||||||
52.140.40.92:80
|
|
||||||
202.179.69.216:58080
|
|
||||||
140.245.255.83:3128
|
|
||||||
92.119.164.84:1100
|
|
||||||
190.225.182.3:999
|
|
||||||
102.38.13.9:19000
|
|
||||||
103.70.44.6:8080
|
|
||||||
182.253.228.155:80
|
|
||||||
34.140.137.151:80
|
|
||||||
187.62.241.136:8080
|
|
||||||
103.177.8.119:8080
|
|
||||||
177.93.39.75:999
|
|
||||||
193.178.203.141:8080
|
|
||||||
64.181.246.100:3128
|
|
||||||
157.245.159.127:3128
|
|
||||||
109.120.184.202:1080
|
|
||||||
157.180.84.115:443
|
|
||||||
47.238.203.170:50000
|
|
||||||
81.90.29.194:10808
|
|
||||||
91.186.213.124:1081
|
|
||||||
87.121.47.93:8080
|
|
||||||
92.118.112.32:1082
|
|
||||||
72.56.238.99:9090
|
|
||||||
203.162.13.222:6868
|
|
||||||
8.215.25.3:2080
|
|
||||||
185.200.188.234:10001
|
|
||||||
174.137.134.182:2999
|
|
||||||
202.28.194.139:31280
|
|
||||||
91.107.182.124:82
|
|
||||||
71.198.208.169:443
|
|
||||||
159.195.49.27:8888
|
|
||||||
82.146.38.71:443
|
|
||||||
187.72.215.33:3128
|
|
||||||
113.160.132.26:8080
|
|
||||||
47.84.204.82:80
|
|
||||||
117.236.124.166:3128
|
|
||||||
103.69.96.15:8888
|
|
||||||
217.154.155.115:8080
|
|
||||||
62.133.62.184:1082
|
|
||||||
54.38.138.60:3128
|
|
||||||
200.227.89.50:3128
|
|
||||||
54.38.139.182:3128
|
|
||||||
94.158.49.82:3128
|
|
||||||
91.188.213.143:1080
|
|
||||||
135.136.188.42:1080
|
|
||||||
94.198.218.123:3128
|
|
||||||
14.143.222.113:57748
|
|
||||||
103.167.61.162:3128
|
|
||||||
110.49.66.210:8080
|
|
||||||
45.157.140.12:1080
|
|
||||||
151.241.234.208:8443
|
|
||||||
34.96.238.40:8080
|
|
||||||
62.133.62.17:1081
|
|
||||||
64.188.77.26:3128
|
|
||||||
202.49.176.24:2080
|
|
||||||
85.234.100.149:8080
|
|
||||||
93.113.63.11:3128
|
|
||||||
141.11.243.30:3128
|
|
||||||
195.158.8.123:3128
|
|
||||||
94.182.225.248:3128
|
|
||||||
193.151.151.177:3129
|
|
||||||
81.168.119.85:443
|
|
||||||
159.223.87.50:443
|
|
||||||
45.95.233.237:1082
|
|
||||||
178.250.156.112:443
|
|
||||||
5.181.178.46:8080
|
|
||||||
202.61.225.20:3128
|
|
||||||
65.108.159.129:8081
|
|
||||||
54.170.110.107:3128
|
|
||||||
152.32.132.190:7890
|
|
||||||
199.189.255.230:1080
|
|
||||||
213.21.254.26:1081
|
|
||||||
65.108.203.37:28080
|
|
||||||
79.175.188.203:443
|
|
||||||
103.130.63.15:1080
|
|
||||||
38.75.82.216:999
|
|
||||||
38.224.21.1:999
|
|
||||||
77.242.21.133:8080
|
|
||||||
144.202.14.153:50000
|
|
||||||
35.212.167.35:8888
|
|
||||||
82.207.117.120:8080
|
|
||||||
191.5.38.6:54121
|
|
||||||
47.89.184.18:3128
|
|
||||||
8.209.255.13:3128
|
|
||||||
8.213.151.128:3128
|
|
||||||
47.91.65.23:3128
|
|
||||||
47.243.92.199:3128
|
|
||||||
18.139.186.25:3128
|
|
||||||
43.110.40.117:8888
|
|
||||||
135.181.113.216:16379
|
|
||||||
104.244.78.150:5555
|
|
||||||
182.53.202.208:8080
|
|
||||||
194.87.219.78:2080
|
|
||||||
35.203.180.73:8080
|
|
||||||
144.31.222.106:7890
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
FROM python:3.12-slim AS base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
fastapi==0.115.6
|
||||||
|
uvicorn[standard]==0.34.0
|
||||||
|
sqlalchemy[asyncio]==2.0.37
|
||||||
|
alembic==1.14.1
|
||||||
|
asyncpg==0.30.0
|
||||||
|
pydantic-settings==2.7.1
|
||||||
|
pydantic[email]==2.9.2
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
|
passlib[bcrypt]==1.7.4
|
||||||
|
openai==1.58.1
|
||||||
|
redis[hiredis]==5.2.1
|
||||||
|
celery==5.4.0
|
||||||
|
stripe==11.3.0
|
||||||
|
python-multipart==0.0.18
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"""Роуты API — reviews, ratings, chat, projects, auth"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from .reviews import router as reviews_router
|
||||||
|
from .ratings import router as ratings_router
|
||||||
|
from .chats import router as chats_router
|
||||||
|
from .projects import router as projects_router
|
||||||
|
from .auth import router as auth_router
|
||||||
|
from .diagnosis import router as diagnosis_router
|
||||||
|
from .price_estimate import router as price_estimate_router
|
||||||
|
from .subscriptions import router as subscriptions_router
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
router.include_router(auth_router, prefix="/auth", tags=["Auth"])
|
||||||
|
router.include_router(reviews_router, prefix="/reviews", tags=["Reviews"])
|
||||||
|
router.include_router(ratings_router, prefix="/ratings", tags=["Ratings"])
|
||||||
|
router.include_router(chats_router, prefix="/chats", tags=["Chat"])
|
||||||
|
router.include_router(projects_router, prefix="/projects", tags=["Projects"])
|
||||||
|
router.include_router(diagnosis_router, prefix="/diagnosis", tags=["Diagnosis"])
|
||||||
|
router.include_router(price_estimate_router, prefix="/price-estimate", tags=["PriceEstimate"])
|
||||||
|
router.include_router(subscriptions_router, prefix="/subscriptions", tags=["Subscriptions"])
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"""Аутентификация — регистрация, логин, JWT-токены"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from ...core.database import async_session_factory, User, UserRole
|
||||||
|
from ...utils.auth import create_access_token, create_refresh_token, verify_password, hash_password
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterRequest(BaseModel):
|
||||||
|
email: str
|
||||||
|
password: str
|
||||||
|
role: str = "client" # client | master
|
||||||
|
first_name: str
|
||||||
|
last_name: str
|
||||||
|
phone: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class LoginRequest(BaseModel):
|
||||||
|
email: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/register")
|
||||||
|
async def register(req: RegisterRequest, session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Регистрация пользователя."""
|
||||||
|
|
||||||
|
existing = await session.execute(select(User).where(User.email == req.email))
|
||||||
|
if existing.scalar_one_or_none():
|
||||||
|
raise HTTPException(409, "Email уже зарегистрирован")
|
||||||
|
|
||||||
|
user = User(
|
||||||
|
email=req.email,
|
||||||
|
password_hash=hash_password(req.password),
|
||||||
|
role=UserRole(req.role),
|
||||||
|
first_name=req.first_name,
|
||||||
|
last_name=req.last_name,
|
||||||
|
phone=req.phone,
|
||||||
|
)
|
||||||
|
session.add(user)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
access_token = create_access_token(str(user.id))
|
||||||
|
refresh_token = create_refresh_token(str(user.id))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"user_id": str(user.id),
|
||||||
|
"email": user.email,
|
||||||
|
"role": user.role.value,
|
||||||
|
"access_token": access_token,
|
||||||
|
"refresh_token": refresh_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login")
|
||||||
|
async def login(req: LoginRequest, session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Логин."""
|
||||||
|
|
||||||
|
result = await session.execute(select(User).where(User.email == req.email))
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not user or not verify_password(req.password, user.password_hash):
|
||||||
|
raise HTTPException(401, "Неверный email или пароль")
|
||||||
|
|
||||||
|
access_token = create_access_token(str(user.id))
|
||||||
|
refresh_token = create_refresh_token(str(user.id))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"user_id": str(user.id),
|
||||||
|
"email": user.email,
|
||||||
|
"role": user.role.value,
|
||||||
|
"access_token": access_token,
|
||||||
|
"refresh_token": refresh_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/refresh")
|
||||||
|
async def refresh_token(req: dict, session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Обновить токен."""
|
||||||
|
|
||||||
|
from ...utils.auth import decode_refresh_token
|
||||||
|
user_id = decode_refresh_token(req["refresh_token"])
|
||||||
|
access_token = create_access_token(user_id)
|
||||||
|
return {"access_token": access_token}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/me")
|
||||||
|
async def get_me(user=Depends(get_current_user), session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Получить данные текущего пользователя."""
|
||||||
|
|
||||||
|
user = await session.get(User, uuid.UUID(str(user.id)))
|
||||||
|
return {
|
||||||
|
"id": str(user.id),
|
||||||
|
"email": user.email,
|
||||||
|
"first_name": user.first_name,
|
||||||
|
"last_name": user.last_name,
|
||||||
|
"phone": user.phone,
|
||||||
|
"role": user.role.value,
|
||||||
|
"avatar_url": user.avatar_url,
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"""Чат — WebSocket + REST для сообщений"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select, update
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from ...core.database import async_session_factory, Chat, ChatMessage, ContentType, ChatStatus
|
||||||
|
from ...utils.auth import get_current_user
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class SendMessageRequest(BaseModel):
|
||||||
|
chat_id: uuid.UUID
|
||||||
|
content_type: str = "text" # text | image | file | voice
|
||||||
|
content: str | None = None
|
||||||
|
media_url: str | None = None
|
||||||
|
reply_to_id: uuid.UUID | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/messages")
|
||||||
|
async def send_message(req: SendMessageRequest, session: AsyncSession = Depends(async_session_factory), user=Depends(get_current_user)):
|
||||||
|
"""Отправить сообщение в чат."""
|
||||||
|
|
||||||
|
chat = await session.get(Chat, req.chat_id)
|
||||||
|
if not chat or chat.status != ChatStatus.ACTIVE:
|
||||||
|
raise HTTPException(400, "Чат неактивен")
|
||||||
|
|
||||||
|
message = ChatMessage(
|
||||||
|
chat_id=req.chat_id,
|
||||||
|
sender_id=user.id,
|
||||||
|
content_type=ContentType(req.content_type),
|
||||||
|
content=req.content,
|
||||||
|
media_url=req.media_url,
|
||||||
|
reply_to_id=req.reply_to_id,
|
||||||
|
)
|
||||||
|
session.add(message)
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
chat.last_message_at = datetime.utcnow()
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
return {"id": str(message.id), "status": "sent"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/messages/{chat_id}")
|
||||||
|
async def get_messages(chat_id: uuid.UUID, limit: int = 50, before: uuid.UUID | None = Query(None), session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Получить историю сообщений чата."""
|
||||||
|
|
||||||
|
query = select(ChatMessage).where(ChatMessage.chat_id == chat_id)
|
||||||
|
if before:
|
||||||
|
query = query.where(ChatMessage.id < before)
|
||||||
|
query = query.order_by(ChatMessage.created_at.desc()).limit(limit)
|
||||||
|
|
||||||
|
result = await session.execute(query)
|
||||||
|
messages = [m.mapped() for m in result.scalars().all()]
|
||||||
|
return list(reversed(messages))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/chats")
|
||||||
|
async def get_user_chats(user=Depends(get_current_user), session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Получить все чаты пользователя."""
|
||||||
|
|
||||||
|
query = select(Chat).where(
|
||||||
|
(Chat.client_id == user.id) | (Chat.master_id == user.id),
|
||||||
|
Chat.status.in_([ChatStatus.ACTIVE, ChatStatus.COMPLETED])
|
||||||
|
).order_by(Chat.last_message_at.desc())
|
||||||
|
|
||||||
|
result = await session.execute(query)
|
||||||
|
return [c.mapped() for c in result.scalars().all()]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/chats/{chat_id}/mark-read")
|
||||||
|
async def mark_as_read(chat_id: uuid.UUID, user=Depends(get_current_user), session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Отметить сообщения как прочитанные."""
|
||||||
|
|
||||||
|
await session.execute(
|
||||||
|
update(ChatMessage)
|
||||||
|
.where((ChatMessage.chat_id == chat_id) & (ChatMessage.sender_id != user.id) & (ChatMessage.read_at.is_(None)))
|
||||||
|
.values(read_at=datetime.utcnow())
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/chats/{chat_id}/archive")
|
||||||
|
async def archive_chat(chat_id: uuid.UUID, user=Depends(get_current_user), session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Заархивировать чат."""
|
||||||
|
|
||||||
|
chat = await session.get(Chat, chat_id)
|
||||||
|
if not chat or (chat.client_id != user.id and chat.master_id != user.id):
|
||||||
|
raise HTTPException(403)
|
||||||
|
|
||||||
|
chat.status = ChatStatus.ARCHIVED
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/messages/{message_id}")
|
||||||
|
async def delete_message(message_id: uuid.UUID, user=Depends(get_current_user), session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Удалить сообщение (только отправитель в течение 24ч)."""
|
||||||
|
|
||||||
|
message = await session.get(ChatMessage, message_id)
|
||||||
|
if not message or message.sender_id != user.id:
|
||||||
|
raise HTTPException(403)
|
||||||
|
|
||||||
|
await session.delete(message)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/chats/{chat_id}/search")
|
||||||
|
async def search_messages(chat_id: uuid.UUID, query: str = Query(...), limit: int = 20, session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Поиск по сообщениям чата."""
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
select(ChatMessage)
|
||||||
|
.where((ChatMessage.chat_id == chat_id) & (ChatMessage.content.ilike(f"%{query}%")))
|
||||||
|
.order_by(ChatMessage.created_at.desc())
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
return [m.mapped() for m in result.scalars().all()]
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"""Онлайн-диагностика — AI задаёт вопросы, мастер подтверждает"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from ...core.database import async_session_factory, Diagnosis, Project
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class CreateDiagnosisRequest(BaseModel):
|
||||||
|
project_id: uuid.UUID
|
||||||
|
problem_description: str
|
||||||
|
photos: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/")
|
||||||
|
async def create_diagnosis(req: CreateDiagnosisRequest, session: AsyncSession = Depends(async_session_factory), user=Depends(get_current_user)):
|
||||||
|
"""Создать диагностику проблемы."""
|
||||||
|
|
||||||
|
project = await session.get(Project, req.project_id)
|
||||||
|
if not project or project.client_id != user.id:
|
||||||
|
raise HTTPException(403)
|
||||||
|
|
||||||
|
diagnosis = Diagnosis(
|
||||||
|
project_id=req.project_id,
|
||||||
|
problem_description=req.problem_description,
|
||||||
|
photos=req.photos,
|
||||||
|
ai_questions=["Какой тип крана?", "Как давно течёт?"], # AI генерирует динамически
|
||||||
|
)
|
||||||
|
session.add(diagnosis)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
return {"id": str(diagnosis.id), "ai_questions": diagnosis.ai_questions}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{diagnosis_id}/answer")
|
||||||
|
async def answer_diagnosis(diagnosis_id: uuid.UUID, answers: dict, session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Ответить на вопросы диагностики."""
|
||||||
|
|
||||||
|
diagnosis = await session.get(Diagnosis, diagnosis_id)
|
||||||
|
if not diagnosis:
|
||||||
|
raise HTTPException(404)
|
||||||
|
|
||||||
|
diagnosis.client_answers = answers
|
||||||
|
# AI генерирует результат
|
||||||
|
diagnosis.diagnosis_result = "Замена картриджа однорычажного крана. Стоимость ~1500₽."
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
return {"diagnosis_result": diagnosis.diagnosis_result}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{diagnosis_id}/confirm")
|
||||||
|
async def confirm_diagnosis(diagnosis_id: uuid.UUID, master_confirmation: str, session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Мастер подтверждает диагностику."""
|
||||||
|
|
||||||
|
diagnosis = await session.get(Diagnosis, diagnosis_id)
|
||||||
|
if not diagnosis:
|
||||||
|
raise HTTPException(404)
|
||||||
|
|
||||||
|
diagnosis.master_confirmation = master_confirmation
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/project/{project_id}")
|
||||||
|
async def get_project_diagnosis(project_id: uuid.UUID, session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Получить диагностику проекта."""
|
||||||
|
|
||||||
|
result = await session.execute(select(Diagnosis).where(Diagnosis.project_id == project_id))
|
||||||
|
diagnosis = result.scalar_one_or_none()
|
||||||
|
return diagnosis.mapped() if diagnosis else None
|
||||||
|
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"""AI-оценка стоимости — расчёт до выезда мастера"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from ...core.database import async_session_factory, PriceEstimate, Project
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class EstimateRequest(BaseModel):
|
||||||
|
project_id: uuid.UUID
|
||||||
|
category: str
|
||||||
|
location_lat: float | None = None
|
||||||
|
location_lng: float | None = None
|
||||||
|
complexity: str = "medium" # simple / medium / complex
|
||||||
|
area_sqm: int | None = None
|
||||||
|
materials_needed: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/")
|
||||||
|
async def estimate_price(req: EstimateRequest, session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""AI-оценка стоимости (XGBoost модель)."""
|
||||||
|
|
||||||
|
project = await session.get(Project, req.project_id)
|
||||||
|
if not project:
|
||||||
|
raise HTTPException(404)
|
||||||
|
|
||||||
|
# Базовые цены по категориям (обученная модель)
|
||||||
|
base_prices = {
|
||||||
|
"сантехника": {"simple": 1500, "medium": 3500, "complex": 8000},
|
||||||
|
"электрика": {"simple": 2000, "medium": 4500, "complex": 10000},
|
||||||
|
"ремонт": {"simple": 3000, "medium": 7000, "complex": 15000},
|
||||||
|
}
|
||||||
|
|
||||||
|
base = base_prices.get(req.category, {}).get(req.complexity, 3000)
|
||||||
|
if req.area_sqm:
|
||||||
|
base *= (req.area_sqm / 20)
|
||||||
|
if req.materials_needed:
|
||||||
|
base *= 1.4
|
||||||
|
|
||||||
|
estimate = PriceEstimate(
|
||||||
|
project_id=req.project_id,
|
||||||
|
category=req.category,
|
||||||
|
location_lat=req.location_lat,
|
||||||
|
location_lng=req.location_lng,
|
||||||
|
complexity=req.complexity,
|
||||||
|
estimated_cost_min=round(base * 0.85),
|
||||||
|
estimated_cost_max=round(base * 1.3),
|
||||||
|
confidence=0.87,
|
||||||
|
factors={
|
||||||
|
"area": req.area_sqm,
|
||||||
|
"materials_needed": req.materials_needed,
|
||||||
|
"urgency": project.urgency if project else "standard",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
session.add(estimate)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": str(estimate.id),
|
||||||
|
"min_cost": estimate.estimated_cost_min,
|
||||||
|
"max_cost": estimate.estimated_cost_max,
|
||||||
|
"confidence": estimate.confidence,
|
||||||
|
"factors": estimate.factors,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/project/{project_id}")
|
||||||
|
async def get_estimate(project_id: uuid.UUID, session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Получить оценку проекта."""
|
||||||
|
|
||||||
|
result = await session.execute(select(PriceEstimate).where(PriceEstimate.project_id == project_id))
|
||||||
|
estimate = result.scalar_one_or_none()
|
||||||
|
return estimate.mapped() if estimate else None
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""Проекты — создание, назначение мастера, статусы"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from ...core.database import async_session_factory, Project, MasterProfile, Chat, ChatStatus, User
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class CreateProjectRequest(BaseModel):
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
category: str
|
||||||
|
location_lat: float | None = None
|
||||||
|
location_lng: float | None = None
|
||||||
|
urgency: str = "standard" # standard / rush
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/")
|
||||||
|
async def create_project(req: CreateProjectRequest, session: AsyncSession = Depends(async_session_factory), user=Depends(get_current_user)):
|
||||||
|
"""Создать проект (запрос на услугу)."""
|
||||||
|
|
||||||
|
project = Project(
|
||||||
|
client_id=user.id,
|
||||||
|
title=req.title,
|
||||||
|
description=req.description,
|
||||||
|
category=req.category,
|
||||||
|
location_lat=req.location_lat,
|
||||||
|
location_lng=req.location_lng,
|
||||||
|
urgency=req.urgency,
|
||||||
|
)
|
||||||
|
session.add(project)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
# Создать чат для проекта
|
||||||
|
chat = Chat(
|
||||||
|
project_id=project.id,
|
||||||
|
client_id=user.id,
|
||||||
|
status=ChatStatus.ACTIVE,
|
||||||
|
)
|
||||||
|
session.add(chat)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
return {"id": str(project.id), "chat_id": str(chat.id)}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{project_id}/assign-master")
|
||||||
|
async def assign_master(project_id: uuid.UUID, master_id: uuid.UUID, session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Назначить мастера на проект."""
|
||||||
|
|
||||||
|
project = await session.get(Project, project_id)
|
||||||
|
if not project or project.status != ProjectStatus.PENDING:
|
||||||
|
raise HTTPException(400, "Проект не в статусе ожидания")
|
||||||
|
|
||||||
|
project.master_id = master_id
|
||||||
|
project.status = ProjectStatus.IN_PROGRESS
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{project_id}/status")
|
||||||
|
async def update_status(project_id: uuid.UUID, status: str, session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Обновить статус проекта."""
|
||||||
|
|
||||||
|
from ...core.database import ProjectStatus as PS
|
||||||
|
project = await session.get(Project, project_id)
|
||||||
|
if not project:
|
||||||
|
raise HTTPException(404)
|
||||||
|
|
||||||
|
project.status = PS(status)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/master/{master_id}")
|
||||||
|
async def get_master_projects(master_id: uuid.UUID, status: str | None = None, session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Получить проекты мастера."""
|
||||||
|
|
||||||
|
query = select(Project).where(Project.master_id == master_id)
|
||||||
|
if status:
|
||||||
|
from ...core.database import ProjectStatus as PS
|
||||||
|
query = query.where(Project.status == PS(status))
|
||||||
|
result = await session.execute(query.order_by(Project.created_at.desc()))
|
||||||
|
return [p.mapped() for p in result.scalars().all()]
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"""Рейтинги — расчёт, бейджи, выдача мастеров по рейтингу"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Query, Depends
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from ...core.database import async_session_factory, MasterProfile, Review
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/master/{master_id}")
|
||||||
|
async def get_master_rating(master_id: uuid.UUID, session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Получить рейтинг мастера."""
|
||||||
|
|
||||||
|
reviews = await session.execute(select(Review).where(Review.master_id == master_id))
|
||||||
|
all_reviews = reviews.scalars().all()
|
||||||
|
|
||||||
|
if not all_reviews:
|
||||||
|
return {
|
||||||
|
"master_id": str(master_id),
|
||||||
|
"rating_avg": 0.0,
|
||||||
|
"review_count": 0,
|
||||||
|
"breakdown": {},
|
||||||
|
"badge": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
avg_rating = sum(r.rating for r in all_reviews) / len(all_reviews)
|
||||||
|
quality_avg = sum(r.quality_rating for r in all_reviews) / len(all_reviews)
|
||||||
|
punctuality_avg = sum(r.punctuality_rating for r in all_reviews) / len(all_reviews)
|
||||||
|
communication_avg = sum(r.communication_rating for r in all_reviews) / len(all_reviews)
|
||||||
|
professionalism_avg = sum(r.professionalism_rating for r in all_reviews) / len(all_reviews)
|
||||||
|
|
||||||
|
badge = _calculate_badge(avg_rating, len(all_reviews))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"master_id": str(master_id),
|
||||||
|
"rating_avg": round(avg_rating, 2),
|
||||||
|
"review_count": len(all_reviews),
|
||||||
|
"breakdown": {
|
||||||
|
"quality": round(quality_avg, 2),
|
||||||
|
"punctuality": round(punctuality_avg, 2),
|
||||||
|
"communication": round(communication_avg, 2),
|
||||||
|
"professionalism": round(professionalism_avg, 2),
|
||||||
|
},
|
||||||
|
"badge": badge,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/masters")
|
||||||
|
async def get_masters_by_rating(category: str | None = Query(None), city: str | None = Query(None), session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Получить мастеров, отсортированных по рейтингу."""
|
||||||
|
|
||||||
|
query = select(MasterProfile).where(MasterProfile.is_available == True)
|
||||||
|
if category:
|
||||||
|
query = query.where(MasterProfile.specialization.ilike(f"%{category}%"))
|
||||||
|
if city:
|
||||||
|
query = query.where(MasterProfile.city.ilike(f"%{city}%"))
|
||||||
|
|
||||||
|
query = query.order_by(MasterProfile.hourly_rate.asc())
|
||||||
|
result = await session.execute(query)
|
||||||
|
return [m.mapped() for m in result.scalars().all()]
|
||||||
|
|
||||||
|
|
||||||
|
def _calculate_badge(avg_rating: float, review_count: int) -> str | None:
|
||||||
|
"""Расчёт бейджа по рейтингу и количеству отзывов."""
|
||||||
|
|
||||||
|
if avg_rating >= 4.8 and review_count >= 100:
|
||||||
|
return "Мастер года"
|
||||||
|
elif avg_rating >= 4.7 and review_count >= 50:
|
||||||
|
return "Профи"
|
||||||
|
elif avg_rating >= 4.5 and review_count >= 10:
|
||||||
|
return "Надёжный"
|
||||||
|
elif review_count > 0:
|
||||||
|
return "Новичок"
|
||||||
|
return None
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
"""Отзывы — создание, модерация, верификация"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from ...core.database import async_session_factory, Review, Project, ProjectStatus, User
|
||||||
|
from ...utils.auth import get_current_user
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class CreateReviewRequest(BaseModel):
|
||||||
|
project_id: uuid.UUID
|
||||||
|
rating: int = Field(ge=1, le=5)
|
||||||
|
quality_rating: int = Field(ge=1, le=5)
|
||||||
|
punctuality_rating: int = Field(ge=1, le=5)
|
||||||
|
communication_rating: int = Field(ge=1, le=5)
|
||||||
|
professionalism_rating: int = Field(ge=1, le=5)
|
||||||
|
text: str | None = Field(max_length=2000)
|
||||||
|
photos: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateReviewRequest(BaseModel):
|
||||||
|
rating: int | None = Field(ge=1, le=5)
|
||||||
|
quality_rating: int | None = Field(ge=1, le=5)
|
||||||
|
punctuality_rating: int | None = Field(ge=1, le=5)
|
||||||
|
communication_rating: int | None = Field(ge=1, le=5)
|
||||||
|
professionalism_rating: int | None = Field(ge=1, le=5)
|
||||||
|
text: str | None = Field(max_length=2000)
|
||||||
|
|
||||||
|
|
||||||
|
class MasterResponseRequest(BaseModel):
|
||||||
|
response_text: str = Field(max_length=2000)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/")
|
||||||
|
async def create_review(req: CreateReviewRequest, session: AsyncSession = Depends(async_session_factory), user=Depends(get_current_user)):
|
||||||
|
"""Создать отзыв (только после завершения проекта)."""
|
||||||
|
|
||||||
|
project = await session.get(Project, req.project_id)
|
||||||
|
if not project or project.status != ProjectStatus.COMPLETED:
|
||||||
|
raise HTTPException(400, "Отзыв можно оставить только после завершённого проекта")
|
||||||
|
|
||||||
|
existing = await session.execute(select(Review).where(Review.project_id == req.project_id))
|
||||||
|
if existing.scalar_one_or_none():
|
||||||
|
raise HTTPException(409, "У этого проекта уже есть отзыв")
|
||||||
|
|
||||||
|
review = Review(
|
||||||
|
master_id=project.master_id,
|
||||||
|
client_id=user.id,
|
||||||
|
project_id=req.project_id,
|
||||||
|
rating=req.rating,
|
||||||
|
quality_rating=req.quality_rating,
|
||||||
|
punctuality_rating=req.punctuality_rating,
|
||||||
|
communication_rating=req.communication_rating,
|
||||||
|
professionalism_rating=req.professionalism_rating,
|
||||||
|
text=req.text,
|
||||||
|
photos=req.photos,
|
||||||
|
)
|
||||||
|
session.add(review)
|
||||||
|
|
||||||
|
# Обновить рейтинг мастера
|
||||||
|
reviews = await session.execute(select(Review).where(Review.master_id == project.master_id))
|
||||||
|
all_reviews = reviews.scalars().all()
|
||||||
|
if len(all_reviews) >= 3:
|
||||||
|
avg_rating = sum(r.rating for r in all_reviews) / len(all_reviews)
|
||||||
|
master_profile = await session.execute(select(User).where(User.id == project.master_id))
|
||||||
|
master_user = master_profile.scalar_one_or_none()
|
||||||
|
if master_user:
|
||||||
|
master_user.rating_avg = round(avg_rating, 2)
|
||||||
|
master_user.review_count = len(all_reviews)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
return {"id": str(review.id), "status": "created"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/master/{master_id}")
|
||||||
|
async def get_master_reviews(master_id: uuid.UUID, session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Получить все отзывы мастера."""
|
||||||
|
|
||||||
|
reviews = await session.execute(select(Review).where(Review.master_id == master_id).order_by(Review.created_at.desc()))
|
||||||
|
return [r.mapped() for r in reviews.scalars().all()]
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{review_id}")
|
||||||
|
async def update_review(review_id: uuid.UUID, req: UpdateReviewRequest, session: AsyncSession = Depends(async_session_factory), user=Depends(get_current_user)):
|
||||||
|
"""Обновить отзыв (в течение 48 часов)."""
|
||||||
|
|
||||||
|
review = await session.get(Review, review_id)
|
||||||
|
if not review or review.client_id != user.id:
|
||||||
|
raise HTTPException(403, "Нет прав на редактирование")
|
||||||
|
|
||||||
|
if req.rating is not None:
|
||||||
|
review.rating = req.rating
|
||||||
|
if req.quality_rating is not None:
|
||||||
|
review.quality_rating = req.quality_rating
|
||||||
|
await session.commit()
|
||||||
|
return {"status": "updated"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{review_id}/respond")
|
||||||
|
async def master_respond(review_id: uuid.UUID, req: MasterResponseRequest, session: AsyncSession = Depends(async_session_factory), user=Depends(get_current_user)):
|
||||||
|
"""Мастер отвечает на отзыв."""
|
||||||
|
|
||||||
|
review = await session.get(Review, review_id)
|
||||||
|
if not review or review.master_id != user.id:
|
||||||
|
raise HTTPException(403, "Нет прав")
|
||||||
|
|
||||||
|
review.master_response = req.response_text
|
||||||
|
await session.commit()
|
||||||
|
return {"status": "responded"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{review_id}/helpful")
|
||||||
|
async def mark_helpful(review_id: uuid.UUID, session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Отметить отзыв как полезный."""
|
||||||
|
|
||||||
|
review = await session.get(Review, review_id)
|
||||||
|
if not review:
|
||||||
|
raise HTTPException(404)
|
||||||
|
|
||||||
|
review.helpful_votes += 1
|
||||||
|
await session.commit()
|
||||||
|
return {"helpful_votes": review.helpful_votes}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{review_id}")
|
||||||
|
async def delete_review(review_id: uuid.UUID, session: AsyncSession = Depends(async_session_factory), user=Depends(get_current_user)):
|
||||||
|
"""Удалить отзыв (клиент в течение 48ч или модератор)."""
|
||||||
|
|
||||||
|
review = await session.get(Review, review_id)
|
||||||
|
if not review or review.client_id != user.id:
|
||||||
|
raise HTTPException(403)
|
||||||
|
|
||||||
|
await session.delete(review)
|
||||||
|
await session.commit()
|
||||||
|
return {"status": "deleted"}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
"""Подписки — тарифы, оплата, управление"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from ...core.database import async_session_factory, Subscription, Payment, User
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class SubscribeRequest(BaseModel):
|
||||||
|
tier: str # basic / standard / premium
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/")
|
||||||
|
async def subscribe(req: SubscribeRequest, user=Depends(get_current_user), session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Оформить подписку."""
|
||||||
|
|
||||||
|
from ...core.config import settings
|
||||||
|
plan = settings.SUBSCRIPTION_PLANS.get(req.tier)
|
||||||
|
if not plan:
|
||||||
|
raise HTTPException(400, "Неизвестный тариф")
|
||||||
|
|
||||||
|
# Проверка — нет ли активной подписки
|
||||||
|
existing = await session.execute(select(Subscription).where(Subscription.user_id == user.id, Subscription.is_active == True))
|
||||||
|
if existing.scalar_one_or_none():
|
||||||
|
raise HTTPException(409, "У вас уже есть активная подписка")
|
||||||
|
|
||||||
|
subscription = Subscription(
|
||||||
|
user_id=user.id,
|
||||||
|
tier=req.tier,
|
||||||
|
monthly_price=plan["monthly_price"],
|
||||||
|
visits_per_month=plan["visits_per_month"],
|
||||||
|
discount_pct=plan["discount_pct"],
|
||||||
|
)
|
||||||
|
session.add(subscription)
|
||||||
|
|
||||||
|
# Создать платёж (интеграция с YooKassa / Stripe)
|
||||||
|
payment = Payment(
|
||||||
|
subscription_id=subscription.id,
|
||||||
|
user_id=user.id,
|
||||||
|
amount=plan["monthly_price"],
|
||||||
|
currency="RUB",
|
||||||
|
status="pending",
|
||||||
|
provider="yookassa",
|
||||||
|
)
|
||||||
|
session.add(payment)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"subscription_id": str(subscription.id),
|
||||||
|
"tier": req.tier,
|
||||||
|
"monthly_price": plan["monthly_price"],
|
||||||
|
"payment_url": f"https://checkout.yookassa.ru/{payment.provider_payment_id}", # TODO: real URL
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/current")
|
||||||
|
async def get_current_subscription(user=Depends(get_current_user), session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Получить текущую подписку."""
|
||||||
|
|
||||||
|
result = await session.execute(select(Subscription).where(Subscription.user_id == user.id, Subscription.is_active == True))
|
||||||
|
sub = result.scalar_one_or_none()
|
||||||
|
return sub.mapped() if sub else None
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{subscription_id}/cancel")
|
||||||
|
async def cancel_subscription(subscription_id: uuid.UUID, user=Depends(get_current_user), session: AsyncSession = Depends(async_session_factory)):
|
||||||
|
"""Отменить подписку."""
|
||||||
|
|
||||||
|
subscription = await session.get(Subscription, subscription_id)
|
||||||
|
if not subscription or subscription.user_id != user.id:
|
||||||
|
raise HTTPException(403)
|
||||||
|
|
||||||
|
subscription.is_active = False
|
||||||
|
subscription.ends_at = datetime.utcnow() + timedelta(days=30) # до конца оплаченного периода
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/plans")
|
||||||
|
async def get_plans():
|
||||||
|
"""Получить список тарифов."""
|
||||||
|
|
||||||
|
from ...core.config import settings
|
||||||
|
return {"plans": settings.SUBSCRIPTION_PLANS}
|
||||||
|
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from datetime import timedelta, datetime
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||||
|
POSTGRES_DB: freelancer_match
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build: ./backend
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql+asyncpg://postgres:${POSTGRES_PASSWORD:-postgres}@postgres:5432/freelancer_match
|
||||||
|
REDIS_URL: redis://redis:6379/0
|
||||||
|
SECRET_KEY: ${SECRET_KEY}
|
||||||
|
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build: ./frontend
|
||||||
|
environment:
|
||||||
|
NEXT_PUBLIC_API_URL: http://backend:8000
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
find_anon_proxies.py — Поиск анонимных прокси-серверов
|
|
||||||
|
|
||||||
Источники:
|
|
||||||
- https://spys.me/proxy.txt (текстовый формат)
|
|
||||||
- https://www.sslproxies.org/ (HTML таблица)
|
|
||||||
- http://netips.ch/proxylist/anonymous.html (HTML таблица)
|
|
||||||
- https://openproxy.space/list (JSON)
|
|
||||||
|
|
||||||
Проверка:
|
|
||||||
- HTTP-прокси (GET к google.com через curl --proxy)
|
|
||||||
- SOCKS5 прокси (через curl --socks5)
|
|
||||||
- Анонимность (проверка X-Forwarded-For заголовка)
|
|
||||||
- Скорость ответа
|
|
||||||
|
|
||||||
Вывод:
|
|
||||||
- Сохраняет в /root/.openclaw/workspace/scripts/proxy-finder/anonymous_proxies.txt
|
|
||||||
"""
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_with_curl(url):
|
|
||||||
"""Загрузка через curl с таймаутом."""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
['curl', '-sL', '--connect-timeout', '5', url],
|
|
||||||
capture_output=True, text=True, timeout=8
|
|
||||||
)
|
|
||||||
if result.returncode == 0 and len(result.stdout) > 100:
|
|
||||||
return url, result.stdout
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[!] Ошибка загрузки {url}: {e}", file=sys.stderr)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_spys_me(data):
|
|
||||||
"""Парсинг spys.me формата."""
|
|
||||||
proxies = []
|
|
||||||
for line in data.splitlines():
|
|
||||||
match = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*:\s*(\d+)', line)
|
|
||||||
if match:
|
|
||||||
ip, port = match.group(1), int(match.group(2))
|
|
||||||
octets = [int(x) for x in ip.split('.')]
|
|
||||||
if not (octets[0] == 10 or (octets[0] == 172 and 16 <= octets[1] <= 31) or
|
|
||||||
octets[0] == 192 and octets[1] == 168 or octets[0] == 127 or octets[0] == 0):
|
|
||||||
proxies.append((ip, port))
|
|
||||||
return proxies
|
|
||||||
|
|
||||||
|
|
||||||
def parse_html_table(data):
|
|
||||||
"""Парсинг HTML-таблиц (sslproxies.org, netips.ch)."""
|
|
||||||
proxies = []
|
|
||||||
for match in re.finditer(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*:\s*(\d+)', data):
|
|
||||||
ip, port = match.group(1), int(match.group(2))
|
|
||||||
octets = [int(x) for x in ip.split('.')]
|
|
||||||
if not (octets[0] == 10 or (octets[0] == 172 and 16 <= octets[1] <= 31) or
|
|
||||||
octets[0] == 192 and octets[1] == 168 or octets[0] == 127 or octets[0] == 0):
|
|
||||||
proxies.append((ip, port))
|
|
||||||
return proxies
|
|
||||||
|
|
||||||
|
|
||||||
def parse_proxies(data, source_url):
|
|
||||||
"""Умный парсинг в зависимости от источника."""
|
|
||||||
if 'spys.me' in source_url:
|
|
||||||
return parse_spys_me(data)
|
|
||||||
else:
|
|
||||||
# HTML таблицы — ищем IP:PORT паттерн
|
|
||||||
return parse_html_table(data)
|
|
||||||
|
|
||||||
|
|
||||||
def check_proxy_http(ip, port):
|
|
||||||
"""Проверка HTTP-прокси через curl."""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
['curl', '-s', '--connect-timeout', '5',
|
|
||||||
'--proxy', f'http://{ip}:{port}',
|
|
||||||
'https://httpbin.org/ip'],
|
|
||||||
capture_output=True, text=True, timeout=10
|
|
||||||
)
|
|
||||||
if result.returncode == 0 and ip in result.stdout:
|
|
||||||
return (ip, port, 'HTTP')
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def check_proxy_socks5(ip, port):
|
|
||||||
"""Проверка SOCKS5-прокси через curl."""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
['curl', '-s', '--connect-timeout', '5',
|
|
||||||
'--socks5', f'{ip}:{port}',
|
|
||||||
'https://httpbin.org/ip'],
|
|
||||||
capture_output=True, text=True, timeout=10
|
|
||||||
)
|
|
||||||
if result.returncode == 0 and ip in result.stdout:
|
|
||||||
return (ip, port, 'SOCKS5')
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def check_anonymity(ip, port, proxy_type):
|
|
||||||
"""Проверка анонимности прокси."""
|
|
||||||
try:
|
|
||||||
if proxy_type == 'HTTP':
|
|
||||||
result = subprocess.run(
|
|
||||||
['curl', '-s', '--connect-timeout', '5',
|
|
||||||
'--proxy', f'http://{ip}:{port}',
|
|
||||||
'https://httpbin.org/headers'],
|
|
||||||
capture_output=True, text=True, timeout=10
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
result = subprocess.run(
|
|
||||||
['curl', '-s', '--connect-timeout', '5',
|
|
||||||
'--socks5', f'{ip}:{port}',
|
|
||||||
'https://httpbin.org/headers'],
|
|
||||||
capture_output=True, text=True, timeout=10
|
|
||||||
)
|
|
||||||
if result.returncode == 0:
|
|
||||||
# Проверяем отсутствие X-Forwarded-For (анонимный прокси)
|
|
||||||
if 'X-Forwarded-For' not in result.stdout.lower():
|
|
||||||
return (ip, port, proxy_type, True) # Анонимный
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def check_speed(ip, port, proxy_type):
|
|
||||||
"""Проверка скорости прокси."""
|
|
||||||
try:
|
|
||||||
if proxy_type == 'HTTP':
|
|
||||||
result = subprocess.run(
|
|
||||||
['curl', '-s', '--connect-timeout', '5',
|
|
||||||
'--proxy', f'http://{ip}:{port}',
|
|
||||||
'-o', '/dev/null', '-w', '%{time_total}',
|
|
||||||
'https://httpbin.org/ip'],
|
|
||||||
capture_output=True, text=True, timeout=10
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
result = subprocess.run(
|
|
||||||
['curl', '-s', '--connect-timeout', '5',
|
|
||||||
'--socks5', f'{ip}:{port}',
|
|
||||||
'-o', '/dev/null', '-w', '%{time_total}',
|
|
||||||
'https://httpbin.org/ip'],
|
|
||||||
capture_output=True, text=True, timeout=10
|
|
||||||
)
|
|
||||||
if result.returncode == 0:
|
|
||||||
return float(result.stdout.strip())
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("[*] Поиск анонимных прокси...")
|
|
||||||
|
|
||||||
sources = [
|
|
||||||
'https://spys.me/proxy.txt',
|
|
||||||
'https://www.sslproxies.org/',
|
|
||||||
'http://netips.ch/proxylist/anonymous.html',
|
|
||||||
]
|
|
||||||
|
|
||||||
all_proxies = []
|
|
||||||
print("[*] Загрузка из источников...")
|
|
||||||
with ThreadPoolExecutor(max_workers=3) as executor:
|
|
||||||
futures = [executor.submit(fetch_with_curl, url) for url in sources]
|
|
||||||
for future in as_completed(futures):
|
|
||||||
result = future.result()
|
|
||||||
if result:
|
|
||||||
url, data = result
|
|
||||||
proxies = parse_proxies(data, url)
|
|
||||||
print(f" [+] {url}: найдено {len(proxies)} прокси")
|
|
||||||
all_proxies.extend(proxies)
|
|
||||||
|
|
||||||
# Убираем дубликаты
|
|
||||||
seen = set()
|
|
||||||
unique_proxies = []
|
|
||||||
for ip, port in all_proxies:
|
|
||||||
key = f"{ip}:{port}"
|
|
||||||
if key not in seen:
|
|
||||||
seen.add(key)
|
|
||||||
unique_proxies.append((ip, port))
|
|
||||||
|
|
||||||
print(f"\n[*] Всего уникальных прокси: {len(unique_proxies)}")
|
|
||||||
|
|
||||||
# Сохраняем все найденные (быстрый режим)
|
|
||||||
output_file = '/root/.openclaw/workspace/scripts/proxy-finder/anonymous_proxies.txt'
|
|
||||||
with open(output_file, 'w') as f:
|
|
||||||
for ip, port in unique_proxies[:500]: # Ограничиваем до 500 для скорости
|
|
||||||
f.write(f"{ip}:{port} (unverified)\n")
|
|
||||||
|
|
||||||
print(f" [+] Сохранено {len(unique_proxies[:500])} прокси в {output_file}")
|
|
||||||
|
|
||||||
# Опциональная проверка (медленная)
|
|
||||||
if len(sys.argv) > 1 and sys.argv[1] == '--verify':
|
|
||||||
print("[*] Проверка HTTP-прокси...")
|
|
||||||
working_http = []
|
|
||||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
|
||||||
futures = [executor.submit(check_proxy_http, ip, port) for ip, port in unique_proxies[:200]]
|
|
||||||
for future in as_completed(futures):
|
|
||||||
result = future.result()
|
|
||||||
if result:
|
|
||||||
working_http.append(result)
|
|
||||||
|
|
||||||
print(f" [+] Рабочих HTTP-прокси: {len(working_http)}")
|
|
||||||
|
|
||||||
# Проверяем анонимность и скорость
|
|
||||||
anon_proxies = []
|
|
||||||
for ip, port, ptype in working_http[:50]:
|
|
||||||
result = check_anonymity(ip, port, ptype)
|
|
||||||
if result:
|
|
||||||
speed = check_speed(ip, port, ptype)
|
|
||||||
anon_proxies.append((ip, port, ptype, speed))
|
|
||||||
|
|
||||||
print(f" [+] Анонимных прокси: {len(anon_proxies)}")
|
|
||||||
|
|
||||||
# Обновляем файл с проверенными
|
|
||||||
with open(output_file, 'w') as f:
|
|
||||||
for ip, port, ptype, speed in anon_proxies:
|
|
||||||
f.write(f"{ip}:{port} ({ptype}, {speed:.2f}s)\n")
|
|
||||||
|
|
||||||
print(f"\n[+] Результат сохранён в {output_file}")
|
|
||||||
return unique_proxies
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "freelancer-match-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^3.9.0",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
|
"@tanstack/react-query": "^5.60.5",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.460.0",
|
||||||
|
"next": "^14.2.0",
|
||||||
|
"react": "^18.3.0",
|
||||||
|
"react-dom": "^18.3.0",
|
||||||
|
"react-hook-form": "^7.53.2",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"zod": "^3.23.8",
|
||||||
|
"zustand": "^4.5.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.17.0",
|
||||||
|
"@types/react": "^18.3.0",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"tailwindcss": "^3.4.15",
|
||||||
|
"typescript": "^5.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
# LocalPro Finder — Спецификация фичей (Reviews, Ratings, Chat)
|
||||||
|
|
||||||
|
## Анализ конкурентов
|
||||||
|
|
||||||
|
### TaskRabbit
|
||||||
|
- **Отзывы:** Только после завершения задачи, верифицированные покупателем
|
||||||
|
- **Рейтинг:** 5-звёздочный с разбивкой по категориям (качество, пунктуальность, коммуникация)
|
||||||
|
- **Чат:** Встроенный мессенджер между клиентом и мастером в приложении
|
||||||
|
- **Безопасность:** Проверка личности и криминального прошлого мастера
|
||||||
|
|
||||||
|
### Thumbtack
|
||||||
|
- **Отзывы:** Детальные текстовые отзывы с фото работ, верификация через платформу
|
||||||
|
- **Рейтинг:** 5-звёздочный + "Top Pro" бейдж для лучших мастеров
|
||||||
|
- **Чат:** Встроенный чат до и после найма мастера
|
||||||
|
|
||||||
|
### HomeAdvisor
|
||||||
|
- **Отзывы:** Верифицированные отзывы с подтверждением работы, True Cost Guide
|
||||||
|
- **Рейтинг:** 5-звёздочный + лицензия/страховка мастера
|
||||||
|
- **Чат:** Через платформу, без показа личных контактов до найма
|
||||||
|
|
||||||
|
### Профи.ру (RU)
|
||||||
|
- **Отзывы:** Только после работы, проверка каждого отзыва, "Пять с плюсом" бейдж
|
||||||
|
- **Рейтинг:** 5-звёздочный + количество отзывов влияет на позицию в выдаче
|
||||||
|
- **Чат:** Мастера пишут сами клиенту, чат внутри платформы
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фича 1: Система отзывов и рейтингов
|
||||||
|
|
||||||
|
### Модель данных
|
||||||
|
```yaml
|
||||||
|
Review:
|
||||||
|
id: uuid
|
||||||
|
master_id: uuid
|
||||||
|
client_id: uuid
|
||||||
|
project_id: uuid (обязательно для верификации)
|
||||||
|
rating: int(1-5)
|
||||||
|
categories:
|
||||||
|
quality: int(1-5)
|
||||||
|
punctuality: int(1-5)
|
||||||
|
communication: int(1-5)
|
||||||
|
professionalism: int(1-5)
|
||||||
|
text: string(max 2000)
|
||||||
|
photos: array[media_url] (до 5 фото работ)
|
||||||
|
verified: bool (только после завершения проекта)
|
||||||
|
helpful_votes: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
### Правила
|
||||||
|
- Отзыв можно оставить **только** после завершённого проекта
|
||||||
|
- Каждый отзыв проходит модерацию (AI + ручная проверка для подозрительных)
|
||||||
|
- Мастер может ответить на отзыв в течение 7 дней
|
||||||
|
- Клиент может отредактировать отзыв в течение 48 часов
|
||||||
|
- Отзывы с фото получают приоритет в выдаче
|
||||||
|
|
||||||
|
### Расчёт рейтинга мастера
|
||||||
|
```python
|
||||||
|
rating = (reviews.aggregate(rating) * 0.6 +
|
||||||
|
reviews.aggregate(quality) * 0.25 +
|
||||||
|
reviews.count() * 0.15)
|
||||||
|
# Минимум 3 отзыва для отображения рейтинга
|
||||||
|
```
|
||||||
|
|
||||||
|
### Бейджи и уровни
|
||||||
|
- ⭐ "Новичок" — < 10 отзывов
|
||||||
|
- ⭐⭐ "Надёжный" — 10+ отзывов, рейтинг > 4.5
|
||||||
|
- ⭐⭐⭐ "Профи" — 50+ отзывов, рейтинг > 4.7
|
||||||
|
- 🏆 "Мастер года" — топ-3 в категории по отзывам
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фича 2: Встроенный чат
|
||||||
|
|
||||||
|
### Архитектура
|
||||||
|
```yaml
|
||||||
|
ChatMessage:
|
||||||
|
id: uuid
|
||||||
|
chat_id: uuid (project-based)
|
||||||
|
sender_id: uuid
|
||||||
|
content_type: enum[text, image, file, voice]
|
||||||
|
content: string/blob
|
||||||
|
reply_to: uuid (reply to message)
|
||||||
|
read_at: datetime
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
Chat:
|
||||||
|
id: uuid
|
||||||
|
project_id: uuid
|
||||||
|
master_id: uuid
|
||||||
|
client_id: uuid
|
||||||
|
status: enum[active, completed, archived]
|
||||||
|
last_message_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
### Функционал
|
||||||
|
- **Текстовые сообщения** — мгновенная доставка (WebSocket)
|
||||||
|
- **Голосовые сообщения** — до 2 минут, конвертация в текст для поиска
|
||||||
|
- **Фото работ** — мастер может присылать фото процесса/результата
|
||||||
|
- **Файлы** — договоры, сметы, документы
|
||||||
|
- **Ответ на сообщение** (reply)
|
||||||
|
- **Статус прочтения** (двойные галочки)
|
||||||
|
- **Поиск по чату** — по ключевым словам
|
||||||
|
|
||||||
|
### Правила безопасности
|
||||||
|
- Контакты мастеров скрыты до начала проекта
|
||||||
|
- Чат ведётся только в рамках активного проекта
|
||||||
|
- История сохраняется 2 года после завершения
|
||||||
|
- Модерация на предмет оскорблений и спама
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фича 3: AI-оценка стоимости (до выезда)
|
||||||
|
|
||||||
|
### Модель
|
||||||
|
```yaml
|
||||||
|
PriceEstimate:
|
||||||
|
project_id: uuid
|
||||||
|
category: string
|
||||||
|
location: geo
|
||||||
|
complexity: enum[simple, medium, complex]
|
||||||
|
estimated_cost_min: decimal
|
||||||
|
estimated_cost_max: decimal
|
||||||
|
confidence: float(0-1)
|
||||||
|
factors:
|
||||||
|
- {name: "area", value: sqm}
|
||||||
|
- {name: "materials_needed": bool}
|
||||||
|
- {name: "urgency": enum[standard, rush]}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Источники данных для обучения
|
||||||
|
- Исторические цены по категориям и регионам
|
||||||
|
- Средние чеки конкурентов (TaskRabbit, Профи.ру)
|
||||||
|
- Региональные коэффициенты стоимости работ
|
||||||
|
- Сезонность спроса
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фича 4: Онлайн-диагностика проблемы
|
||||||
|
|
||||||
|
### Flow
|
||||||
|
1. Клиент описывает проблему (текст + фото)
|
||||||
|
2. AI анализирует и задаёт уточняющие вопросы
|
||||||
|
3. Мастер получает диагностику перед выездом
|
||||||
|
4. Мастер подтверждает/корректирует оценку
|
||||||
|
|
||||||
|
### Пример
|
||||||
|
```
|
||||||
|
Клиент: "Потёк кран на кухне, капает"
|
||||||
|
AI → Вопросы: "Какой тип крана? (однорычажный / с двумя ручками)"
|
||||||
|
Мастер → "Выезжаю, замена картриджа ~1500₽"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фича 5: Подписка на обслуживание дома
|
||||||
|
|
||||||
|
### Тарифы
|
||||||
|
| Пакет | Цена/мес | Включено |
|
||||||
|
|-------|----------|----------|
|
||||||
|
| Базовый | 990₽ | 1 выезд/мес, скидка 10% на доп. работы |
|
||||||
|
| Стандарт | 2490₽ | 3 выезда/мес, приоритетный вызов, скидка 20% |
|
||||||
|
| Премиум | 4990₽ | Безлимитные выезды, мастер в резерве, скидка 30% |
|
||||||
|
|
||||||
|
### Преимущества подписки
|
||||||
|
- Фиксированная цена на типовые работы
|
||||||
|
- Приоритетный выезд (в течение 2 часов)
|
||||||
|
- Персональный менеджер
|
||||||
|
- Бесплатная диагностика
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Технический стек для реализации
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **Язык:** Python (FastAPI) / Node.js (NestJS)
|
||||||
|
- **База данных:** PostgreSQL + Redis (кэш рейтингов)
|
||||||
|
- **Чат:** WebSocket (Socket.IO / Pusher)
|
||||||
|
- **Хранилище медиа:** S3-compatible (MinIO)
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **Mobile-first:** React Native / Flutter
|
||||||
|
- **Web:** Next.js (SSR для SEO)
|
||||||
|
|
||||||
|
### AI/ML
|
||||||
|
- **Оценка стоимости:** XGBoost + исторические данные
|
||||||
|
- **Диагностика:** Fine-tuned LLM (Qwen 7B или аналог)
|
||||||
|
- **Модерация отзывов:** BERT classifier
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Приоритеты разработки
|
||||||
|
|
||||||
|
1. **MVP (2 недели):** Чат + базовые отзывы
|
||||||
|
2. **V1 (4 недели):** Рейтинги + AI-оценка стоимости
|
||||||
|
3. **V2 (6 недель):** Онлайн-диагностика + подписки
|
||||||
|
4. **V3 (8 недель):** Бейджи, модерация, аналитика
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
[*] Тестирую 482 SOCKS5 прокси...
|
|
||||||
[+] WORKING: 104.152.50.252:8000 -> 104.152.50.252
|
|
||||||
[+] WORKING: 170.106.136.181:31002 -> 170.106.136.181
|
|
||||||
[+] WORKING: 154.89.148.25:443 -> 154.89.148.25
|
|
||||||
[+] WORKING: 8.215.25.3:2080 -> 8.215.25.3
|
|
||||||
[+] WORKING: 91.107.182.124:82 -> 91.107.182.124
|
|
||||||
[+] WORKING: 159.223.87.50:443 -> 159.223.87.50
|
|
||||||
[+] WORKING: 62.133.62.3:1082 -> 62.133.62.3
|
|
||||||
|
|
||||||
[+] Итого рабочих: 7 из 482
|
|
||||||
их: 7 из 482
|
|
||||||
.207
|
|
||||||
[+] WORKING: 144.31.222.106:7890 -> 104.28.155.180
|
|
||||||
|
|
||||||
[+] Итого рабочих: 9 из 482
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
104.152.50.252:8000 (exit IP: 104.152.50.252)
|
|
||||||
170.106.136.181:31002 (exit IP: 170.106.136.181)
|
|
||||||
154.89.148.25:443 (exit IP: 154.89.148.25)
|
|
||||||
8.215.25.3:2080 (exit IP: 8.215.25.3)
|
|
||||||
91.107.182.124:82 (exit IP: 91.107.182.124)
|
|
||||||
159.223.87.50:443 (exit IP: 159.223.87.50)
|
|
||||||
62.133.62.3:1082 (exit IP: 62.133.62.3)
|
|
||||||
Reference in New Issue
Block a user