💾 Archived View for gemini.rmf-dev.com › repo › Vaati › gmiChat › files › 9cc1c2ce3aba62e30c0177ad93… captured on 2023-05-24 at 18:07:12. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2023-03-20)

➡️ Next capture (2023-09-08)

-=-=-=-=-=-=-

0 require Logger

1 require Ecto.Query

2

3 # Gemini Chat

4

5 defmodule Gmichat do

6

7 @max_register 3

8 @max_attemps_ip 10

9 @max_attemps_account 50

10 @max_messages 3

11 @max_messages_timeout 2

12

13 defp main_page(args) do

14 if get_user(args[:cert]) == nil do

15 Gmi.content(

16 "# GmiChat\n\n" <>

17 "Chat platform for the Gemini protocol\n" <>

18 "A client certificate is required to register and to login\n\n" <>

19 "=>/login Login\n" <>

20 "=>/register Register\n\n" <>

21 "## Softwares\n\n" <>

22 "=>gemini://gemini.rmf-dev.com/repo/Vaati/gmiChat Source code\n" <>

23 "=>gemini://gemini.rmf-dev.com/repo/Vaati/Vgmi/readme Recommended client"

24 )

25 else

26 Gmi.redirect("/account")

27 end

28 end

29

30 defp can_attempt(table, key, threshold) do

31 rows = :ets.lookup(table, key)

32 rows == [] or elem(hd(rows), 1) < threshold

33 end

34

35 defp add_attempt(table, key) do

36 value = :ets.lookup(table, key)

37 value = if value == [] do 0 else elem(hd(value), 1) end

38 :ets.insert(table, {

39 key, value + 1

40 })

41 end

42

43 defp ask_input(args, field, to) do

44 cond do

45 args[:cert] == nil ->

46 Gmi.cert_required("Certificate required to register")

47 args[:query] == "" ->

48 Gmi.input(field)

49 true ->

50 Gmi.redirect(to <> args[:query])

51 end

52 end

53

54 defp write_msg(message, from, dst, dm) do

55 msg = %Gmichat.Message{

56 message: message,

57 user_id: from,

58 destination: dst,

59 timestamp: System.system_time(:second),

60 dm: dm

61 }

62 last_message = :ets.lookup(:messages_rate, from)

63 last_message = if last_message != [] do

64 elem(hd(last_message), 1)

65 else

66 last_message

67 end

68 last_message =

69 if last_message == [] or elem(last_message, 0) + @max_messages_timeout

70 < System.system_time(:second) do

71 {System.system_time(:second), 1}

72 else

73 {elem(last_message, 0), elem(last_message, 1) + 1}

74 end

75 if elem(last_message, 1) > @max_messages do

76 "You sent too many messages in a short period of time"

77 else

78 :ets.insert(:messages_rate, {from, last_message})

79 {state, ret} = msg |> Gmichat.Repo.insert

80 if state == :ok do

81 :ok

82 else

83 elem(elem(hd(ret.errors), 1), 0)

84 end

85 end

86 end

87

88 defp create_user(name, password) do

89 user = %Gmichat.User{

90 name: String.downcase(name),

91 password: password,

92 timezone: 0,

93 linelength: 0,

94 leftmargin: 0,

95 timestamp: System.system_time(:second)

96 }

97 {state, ret} = Gmichat.User.changeset(user, %{}) |> Gmichat.Repo.insert

98 if state == :ok do

99 :ok

100 else

101 to_string(elem(hd(ret.errors), 0)) <> " " <> elem(elem(hd(ret.errors), 1), 0)

102 end

103 end

104

105 defp register_complete(args) do

106 cond do

107 args[:cert] == nil ->

108 Gmi.cert_required("Certificate required to register")

109 args[:query] == "" ->

110 Gmi.input_secret("Password")

111 !can_attempt(:registrations, elem(args[:addr], 0), @max_register) ->

112 Gmi.failure("Temporary registration limit reached for your ip")

113 true ->

114 ret = create_user(args[:name], args[:query])

115 if ret == :ok do

116 add_attempt(:registrations, elem(args[:addr], 0))

117 Gmi.redirect("/register/x/success")

118 else

119 Gmi.failure(ret)

120 end

121

122 end

123 end

124

125 defp try_login(name, password, addr) do

126 name = String.downcase(name)

127 cond do

128 !can_attempt(:account_attempts, name, @max_attemps_account) ->

129 {:error, "Too many login attempts for this account"}

130 !can_attempt(:ip_attempts, addr, @max_attemps_ip) ->

131 {:error, "Too many login attempts from your ip"}

132 true ->

133 user = Gmichat.User |> Gmichat.Repo.get_by(name: name)

134 if user != nil and Argon2.verify_pass(password, user.password) do

135 {:ok, user}

136 else

137 add_attempt(:account_attempts, name)

138 add_attempt(:ip_attempts, addr)

139 {:error, "Invalid username or password"}

140 end

141 end

142 end

143

144 defp login_complete(args) do

145 cond do

146 args[:cert] == nil ->

147 Gmi.cert_required("Certificate required to register")

148 args[:query] == "" ->

149 Gmi.input_secret("Password")

150 true ->

151 {state, ret} = try_login(args[:name], args[:query], elem(args[:addr], 0))

152 if state == :ok do

153 :ets.insert(:users, {

154 args[:cert], %{ret | password: :ignore}

155 })

156 Gmi.redirect("/account")

157 else

158 Gmi.failure(ret)

159 end

160 end

161 end

162

163 defp format_message(message, llength, margin, pos \\ 0) do

164 if llength == 0 do

165 String.duplicate(" ", margin) <> message <> "\n"

166 else

167 lastline = pos + llength >= String.length(message)

168 length = if lastline do

169 String.length(message) - pos

170 else

171 llength

172 end

173 new_message =

174 String.slice(message, 0, pos) <>

175 String.duplicate(" ", margin) <>

176 String.slice(message, pos, length) <> "\n" <>

177 String.slice(message, pos + length, String.length(message) - pos - length)

178 if lastline do

179 new_message

180 else

181 format_message(new_message,

182 llength, margin, pos + llength + margin + 1)

183 end

184 end

185 end

186

187 defp show_messages(rows, timezone, llength, margin, out \\ "") do

188 if rows == [] do

189 out

190 else

191 row = hd(rows)

192 {:ok, time} = DateTime.from_unix(row.timestamp + timezone * 3600)

193 show_messages(tl(rows), timezone, llength, margin,

194 format_message(

195 "[" <> String.slice(DateTime.to_string(time), 0..-2) <> "] "

196 <> "<" <> row.user.name <> "> "

197 <> row.message, llength, margin) <> out)

198 end

199 end

200

201 defp account(_, user) do

202 results = Ecto.Query.from m in Gmichat.Message,

203 order_by: [desc: m.timestamp],

204 limit: 30,

205 where: m.dm == false and m.destination == 0

206

207 results = results |> Ecto.Query.preload(:user) |> Gmichat.Repo.all

208 content = "# Connected as " <> user.name <>

209 "\n\n" <> "## Public chat\n"

210 <> show_messages(results, user.timezone, user.linelength, user.leftmargin)

211 <> "\n=>/account/write Send message"

212 <> "\n=>/account/dm Send direct message"

213 <> "\n=>/account/contacts Contacts"

214 <> "\n\n## Options\n"

215 <> "\n=>/account/zone Set time zone [UTC "

216 <> to_string(user.timezone) <> "]"

217 <> "\n=>/account/llength Set line length ["

218 <> to_string(user.linelength) <> "]"

219 <> "\n=>/account/margin Set left margin ["

220 <> to_string(user.leftmargin) <> "]"

221 <> "\n\n=>/account/disconnect Disconnect"

222 Gmi.content(content)

223 end

224

225 defp account_zone(args, user) do

226 if args[:query] == "" do

227 Gmi.input("UTC offset")

228 else

229 ret = Integer.parse(args[:query])

230 ret = if ret == :error do ret else elem(ret, 0) end

231 cond do

232 ret == :error ->

233 Gmi.bad_request("Invalid value")

234 ret < -14 or ret > 14 ->

235 Gmi.bad_request("Offset must be between -14 and 14")

236 true ->

237 Ecto.Changeset.change(user, %{timezone: ret}) |>

238 Gmichat.User.changeset |>

239 Gmichat.Repo.update!

240 :ets.insert(:users, {args[:cert], %{user | timezone: ret}})

241 Gmi.redirect("/account")

242 end

243 end

244 end

245

246 defp account_line_length(args, user) do

247 if args[:query] == "" do

248 Gmi.input("Chat line length (0 = no limit)")

249 else

250 ret = Integer.parse(args[:query])

251 ret = if ret == :error do ret else elem(ret, 0) end

252 cond do

253 ret == :error ->

254 Gmi.bad_request("Invalid value")

255 ret < 0 or ret > 1024 ->

256 Gmi.bad_request("Line length must be between 0 and 1024")

257 true ->

258 Ecto.Changeset.change(user, %{linelength: ret}) |>

259 Gmichat.User.changeset |>

260 Gmichat.Repo.update!

261 :ets.insert(:users, {args[:cert], %{user | linelength: ret}})

262 Gmi.redirect("/account")

263 end

264 end

265 end

266

267 defp account_left_margin(args, user) do

268 if args[:query] == "" do

269 Gmi.input("Left margin (0 = no margin)")

270 else

271 ret = Integer.parse(args[:query])

272 ret = if ret == :error do ret else elem(ret, 0) end

273 cond do

274 ret == :error ->

275 Gmi.bad_request("Invalid value")

276 ret < 0 or ret > 4096 ->

277 Gmi.bad_request("Left margin must be between 0 and 4096")

278 true ->

279 Ecto.Changeset.change(user, %{leftmargin: ret}) |>

280 Gmichat.User.changeset |>

281 Gmichat.Repo.update!

282 :ets.insert(:users, {args[:cert], %{user | leftmargin: ret}})

283 Gmi.redirect("/account")

284 end

285 end

286 end

287

288 defp account_write(args, user) do

289 if args[:query] == "" do

290 Gmi.input(user.name)

291 else

292 ret = write_msg(args[:query], user.id, 0, false)

293 if ret == :ok do

294 Gmi.redirect("/account")

295 else

296 Gmi.failure(ret)

297 end

298 end

299 end

300

301 def dm(args, user) do

302 to = Gmichat.User |> Gmichat.Repo.get_by(name: String.downcase(args[:name]))

303 if to != nil and user.id != to.id do

304 results = Ecto.Query.from m in Gmichat.Message,

305 order_by: [desc: m.timestamp],

306 limit: 30,

307 where: m.dm == true and

308 (m.destination == ^to.id and m.user_id == ^user.id) or

309 (m.destination == ^user.id and m.user_id == ^to.id)

310 results = results |> Ecto.Query.preload(:user) |> Gmichat.Repo.all

311 Gmi.content(

312 "=>/account/contacts Go back\n\n" <>

313 "# " <> to.name <> " - Direct messages\n\n" <>

314 show_messages(results, user.timezone, user.linelength, user.leftmargin) <>

315 "\n=>/account/dm/" <> to.name <> "/write Send message")

316 else

317 Gmi.bad_request("User " <> args[:name] <> " not found")

318 end

319 end

320

321 def dm_write(args, user) do

322 if args[:query] == "" do

323 Gmi.input("Send message")

324 else

325 to = Gmichat.User |> Gmichat.Repo.get_by(name: args[:name])

326 if to != nil and user.id != to.id do

327 write_msg(args[:query], user.id, to.id, true)

328 Gmi.redirect("/account/dm/" <> to.name)

329 else

330 Gmi.bad_request("User " <> args[:name] <> " not found")

331 end

332 end

333 end

334

335 def show_contacts(rows, out \\ "") do

336 if rows == [] do

337 out

338 else

339 name = hd(hd(rows))

340 show_contacts(tl(rows), out <> "=>/account/dm/" <>

341 name <> " " <> name <> "\n")

342 end

343 end

344

345 def contacts(_, user) do

346 query = """

347 SELECT name FROM

348 (SELECT DISTINCT

349 (CASE WHEN user_id=$1::integer THEN destination ELSE user_id END) AS uid,

350 MAX(timestamp) as max

351 FROM messages WHERE dm = 1 AND (destination = $1::integer OR user_id = $1::integer)

352 GROUP BY uid) dms

353 INNER JOIN users u ON u.id = dms.uid

354 ORDER BY dms.max DESC;

355 """

356

357 results = Ecto.Adapters.SQL.query!(Gmichat.Repo, query, [user.id])

358

359 Gmi.content("=>/account Go back\n\n# Contacts\n\n" <>

360 show_contacts(results.rows))

361 end

362

363 def connected(args, func) do

364 user = get_user(args[:cert])

365 if user == nil do

366 Gmi.redirect("/")

367 else

368 func.(args, user)

369 end

370 end

371

372 def get_user(cert) do

373 if cert == nil do

374 nil

375 else

376 rows = :ets.lookup(:users, cert)

377 if rows == [] do

378 nil

379 else

380 elem(hd(rows), 1)

381 end

382 end

383 end

384

385 defp decrease_limit_iter(table, rows) do

386 if rows != [] do

387 :ets.insert(table, {elem(hd(rows), 0), elem(hd(rows), 1) - 1})

388 decrease_limit_iter(table, tl(rows))

389 end

390 end

391

392 defp decrease_limit() do

393 select = [{{:"$1", :"$2"}, [{:>, :"$2", 0}], [{{:"$1", :"$2"}}]}]

394 decrease_limit_iter(:registrations, :ets.select(:registrations, select))

395 decrease_limit_iter(:account_attempts, :ets.select(:account_attempts, select))

396 decrease_limit_iter(:ip_attempts, :ets.select(:ip_attempts, select))

397 :timer.sleep(30000)

398 decrease_limit()

399 end

400

401 def start() do

402 :users = :ets.new(:users, [:set, :public, :named_table])

403 :registrations = :ets.new(:registrations, [:set, :public, :named_table])

404 :ip_attempts = :ets.new(:ip_attempts, [:set, :public, :named_table])

405 :account_attempts = :ets.new(:account_attempts, [:set, :public, :named_table])

406 :messages_rate = :ets.new(:messages_rate, [:set, :public, :named_table])

407 Gmi.init()

408 Gmi.add_route("/", fn args -> main_page(args) end)

409 Gmi.add_route("/register", fn args -> ask_input(args, "Username", "/register/") end)

410 Gmi.add_route("/register/:name", fn args -> register_complete(args) end)

411 Gmi.add_route("/register/x/success", fn _ ->

412 Gmi.content(

413 "# Registration complete\n\n" <>

414 "=>/login You can now login with your account\n")

415 end)

416 Gmi.add_route("/login", fn args -> ask_input(args, "Username", "/login/") end)

417 Gmi.add_route("/login/:name", fn args -> login_complete(args) end)

418 Gmi.add_route("/account", fn args ->

419 connected(args, fn args, user ->

420 account(args, user)

421 end)

422 end)

423 Gmi.add_route("/account/write", fn args ->

424 connected(args, fn args, user ->

425 account_write(args, user)

426 end)

427 end)

428 Gmi.add_route("/account/zone", fn args ->

429 connected(args, fn args, user ->

430 account_zone(args, user)

431 end)

432 end)

433 Gmi.add_route("/account/llength", fn args ->

434 connected(args, fn args, user ->

435 account_line_length(args, user)

436 end)

437 end)

438 Gmi.add_route("/account/margin", fn args ->

439 connected(args, fn args, user ->

440 account_left_margin(args, user)

441 end)

442 end)

443 Gmi.add_route("/account/dm", fn args ->

444 connected(args, fn args, _ ->

445 ask_input(args, "Username", "/account/dm/")

446 end)

447 end)

448 Gmi.add_route("/account/dm/:name", fn args ->

449 connected(args, fn args, user ->

450 dm(args, user)

451 end)

452 end)

453 Gmi.add_route("/account/dm/:name/write", fn args ->

454 connected(args, fn args, user ->

455 dm_write(args, user)

456 end)

457 end)

458 Gmi.add_route("/account/contacts", fn args ->

459 connected(args, fn args, user ->

460 contacts(args, user)

461 end)

462 end)

463 Gmi.add_route("/account/disconnect", fn args ->

464 if args[:cert] != nil do

465 :ets.delete(:users, args[:cert])

466 end

467 Gmi.redirect("/")

468 end)

469 spawn_link(fn -> decrease_limit() end)

470 Gmi.listen()

471 end

472

473 end

474